From 5dc0fbcd3a72b8aff298136a5fba6f692f7eda78 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Sat, 25 Apr 2026 11:54:43 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20harbor=20=E2=80=94=20proxy=20cache=20?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=BA=D0=B0=D0=BB=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20+=20tag=20retention=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proxy cache (harbor_proxy_cache_enabled: true): - Автоматически создаёт registry endpoints + proxy cache проекты для: docker.io, gcr.io, quay.io, ghcr.io, registry.k8s.io, mcr.microsoft.com, public.ecr.aws - При pull образа через harbor.example.com// он кэшируется - Реализовано через alpine:3.19 + curl + jq Kubernetes Job (вызывает Harbor REST API изнутри кластера) Tag retention (harbor_retention_enabled: true, harbor_retention_max_tags: 3): - Политика "latestPushedN=3" применяется ко ВСЕМ проектам (включая proxy cache) - Пропускает проекты с уже существующей политикой (idempotent) - Запуск: ежедневно в 03:00 UTC (cron schedule в Harbor) Механизм: Job запускается после Helm install, достучивается до harbor-core по internal service DNS, ждёт API готовности (40 попыток × 15 сек = 10 мин max). --- addons/harbor/role/defaults/main.yml | 42 ++++++ addons/harbor/role/tasks/main.yml | 51 ++++++- .../templates/harbor-configure-job.yaml.j2 | 141 ++++++++++++++++++ 3 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 addons/harbor/role/templates/harbor-configure-job.yaml.j2 diff --git a/addons/harbor/role/defaults/main.yml b/addons/harbor/role/defaults/main.yml index 4136af4..a00fa32 100644 --- a/addons/harbor/role/defaults/main.yml +++ b/addons/harbor/role/defaults/main.yml @@ -38,6 +38,48 @@ harbor_redis_storage_size: "1Gi" harbor_metrics_enabled: true # ServiceMonitor создаётся только когда addon_prometheus_stack: true +# ── Proxy Cache (зеркалирование публичных registry) ─────────────────────────── +# При pull образа через Harbor он автоматически кэшируется в Harbor. +# Использование: docker pull harbor.example.com/dockerhub/library/nginx:latest +# ^^^^^^^^^ имя проекта ниже +harbor_proxy_cache_enabled: true +harbor_proxy_cache_registries: + - name: "dockerhub" + description: "Docker Hub" + type: "docker-hub" + url: "https://hub.docker.com" + - name: "gcr" + description: "Google Container Registry" + type: "gcr" + url: "https://gcr.io" + - name: "quay" + description: "Quay.io" + type: "quay" + url: "https://quay.io" + - name: "ghcr" + description: "GitHub Container Registry" + type: "docker-registry" + url: "https://ghcr.io" + - name: "k8s-registry" + description: "Kubernetes Registry (registry.k8s.io)" + type: "docker-registry" + url: "https://registry.k8s.io" + - name: "mcr" + description: "Microsoft Container Registry" + type: "docker-registry" + url: "https://mcr.microsoft.com" + - name: "ecr-public" + description: "Amazon ECR Public Gallery" + type: "docker-registry" + url: "https://public.ecr.aws" + +# ── Tag Retention Policy ────────────────────────────────────────────────────── +# Автоматически удалять образы если их больше N штук для одного репозитория. +# Политика применяется ко ВСЕМ проектам (включая proxy cache). +# Запуск: ежедневно в 03:00 UTC. +harbor_retention_enabled: true +harbor_retention_max_tags: 3 # хранить последних N тегов по дате push + harbor_resources: requests: cpu: 100m diff --git a/addons/harbor/role/tasks/main.yml b/addons/harbor/role/tasks/main.yml index 2cab5ab..31c2793 100644 --- a/addons/harbor/role/tasks/main.yml +++ b/addons/harbor/role/tasks/main.yml @@ -101,12 +101,59 @@ environment: KUBECONFIG: "{{ k3s_kubeconfig_path }}" +- name: Template Harbor configuration Job (proxy cache + retention) + ansible.builtin.template: + src: harbor-configure-job.yaml.j2 + dest: /tmp/harbor-configure-job.yaml + mode: '0644' + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + +- name: Delete previous Harbor configuration Job (если было) + ansible.builtin.command: > + k3s kubectl -n {{ harbor_namespace }} delete job harbor-configure --ignore-not-found=true + changed_when: false + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + +- name: Apply Harbor configuration Job + ansible.builtin.command: k3s kubectl apply -f /tmp/harbor-configure-job.yaml + changed_when: true + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + +- name: Wait for Harbor configuration Job to complete + ansible.builtin.command: > + k3s kubectl -n {{ harbor_namespace }} + wait job/harbor-configure + --for=condition=complete --timeout=600s + changed_when: false + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + +- name: Show Harbor configuration Job logs + ansible.builtin.command: > + k3s kubectl -n {{ harbor_namespace }} logs + -l job-name=harbor-configure --tail=50 + register: _harbor_configure_logs + changed_when: false + failed_when: false + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + +- name: Harbor configuration output + ansible.builtin.debug: + msg: "{{ _harbor_configure_logs.stdout_lines }}" + when: harbor_proxy_cache_enabled | bool or harbor_retention_enabled | bool + - name: Show Harbor access info ansible.builtin.debug: msg: - "Harbor установлен в namespace: {{ harbor_namespace }}" - "URL: http{{ 's' if harbor_ingress_tls else '' }}://{{ harbor_ingress_host }}" - - "Логин: admin" - - "Пароль: {{ harbor_admin_password }}" + - "Логин: admin / Пароль: {{ harbor_admin_password }}" - "Docker login: docker login {{ harbor_ingress_host }} -u admin" - "БД: {{ harbor_database_type }}{{ ' (PostgreSQL ' + harbor_db_host + ')' if harbor_database_type == 'external' else '' }}" + - "" + - "Proxy cache pull (образ кэшируется в Harbor автоматически):" + - " docker pull {{ harbor_ingress_host }}/dockerhub/library/nginx:latest" + - " docker pull {{ harbor_ingress_host }}/gcr/google-containers/pause:3.9" + - " docker pull {{ harbor_ingress_host }}/ghcr/owner/image:tag" + - " docker pull {{ harbor_ingress_host }}/k8s-registry/kube-apiserver:v1.30.0" + - "" + - "Retention policy: хранить последние {{ harbor_retention_max_tags }} тега, запуск в 03:00 UTC" diff --git a/addons/harbor/role/templates/harbor-configure-job.yaml.j2 b/addons/harbor/role/templates/harbor-configure-job.yaml.j2 new file mode 100644 index 0000000..2ef1f68 --- /dev/null +++ b/addons/harbor/role/templates/harbor-configure-job.yaml.j2 @@ -0,0 +1,141 @@ +--- +# ConfigMap со скриптом конфигурации Harbor +# Настраивает proxy-cache registry и tag retention policies через REST API +apiVersion: v1 +kind: ConfigMap +metadata: + name: harbor-configure + namespace: {{ harbor_namespace }} +data: + configure.sh: | + #!/bin/sh + set -e + + apk add -q curl jq + + BASE="http://harbor.{{ harbor_namespace }}.svc.cluster.local" + AUTH="admin:{{ harbor_admin_password }}" + + # ── Ждём готовности Harbor API ──────────────────────────────────────────── + echo ">>> Ожидаю готовности Harbor API..." + for i in $(seq 1 40); do + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -u "$AUTH" \ + "$BASE/api/v2.0/systeminfo" 2>/dev/null || echo "000") + [ "$STATUS" = "200" ] && echo "Harbor API готов" && break + echo "Попытка $i/40 (HTTP $STATUS), жду 15 сек..." + sleep 15 + [ "$i" -eq 40 ] && echo "ОШИБКА: Harbor API недоступен" && exit 1 + done + +{% if harbor_proxy_cache_enabled %} + # ── Proxy Cache: создать registry endpoints и проекты ───────────────────── + echo "" + echo ">>> Настройка proxy-cache реестров..." + +{% for reg in harbor_proxy_cache_registries %} + echo "--- {{ reg.description }} ({{ reg.name }}) ---" + + # Создать registry endpoint (409 = уже существует — ок) + curl -sf -X POST -u "$AUTH" -H "Content-Type: application/json" \ + -d '{"name":"{{ reg.name }}","type":"{{ reg.type }}","url":"{{ reg.url }}","description":"{{ reg.description }}","insecure":false}' \ + "$BASE/api/v2.0/registries" \ + -o /dev/null -w " Endpoint: HTTP %{http_code}\n" || true + + # Получить ID endpoint-а + REG_ID=$(curl -sf -u "$AUTH" \ + "$BASE/api/v2.0/registries?name={{ reg.name }}" | jq -e '.[0].id' 2>/dev/null || echo "") + + if [ -z "$REG_ID" ] || [ "$REG_ID" = "null" ]; then + echo " ПРЕДУПРЕЖДЕНИЕ: не удалось получить ID для {{ reg.name }}, пропускаю" + else + # Создать proxy cache проект (409 = уже существует — ок) + curl -sf -X POST -u "$AUTH" -H "Content-Type: application/json" \ + -d "{\"project_name\":\"{{ reg.name }}\",\"public\":true,\"registry_id\":$REG_ID,\"metadata\":{\"public\":\"true\"}}" \ + "$BASE/api/v2.0/projects" \ + -o /dev/null -w " Проект: HTTP %{http_code}\n" || true + fi + +{% endfor %} + echo ">>> Proxy-cache реестры настроены" + echo " Примеры pull через Harbor:" +{% for reg in harbor_proxy_cache_registries %} + echo " {{ harbor_ingress_host }}/{{ reg.name }}/:" +{% endfor %} +{% endif %} + +{% if harbor_retention_enabled %} + # ── Tag Retention: применить ко всем проектам ───────────────────────────── + echo "" + echo ">>> Настройка retention policy (max {{ harbor_retention_max_tags }} тегов на репозиторий)..." + + # Получить все проекты и сохранить в файл (избегаем subshell через pipe) + curl -sf -u "$AUTH" "$BASE/api/v2.0/projects?page_size=100" \ + | jq -c '.[]' > /tmp/projects.json + + while IFS= read -r project; do + PROJ_ID=$(echo "$project" | jq '.id') + PROJ_NAME=$(echo "$project" | jq -r '.name') + + # Пропустить если retention уже настроен + EXISTING_RET=$(echo "$project" | jq -r '.metadata.retention_id // empty') + if [ -n "$EXISTING_RET" ]; then + echo " $PROJ_NAME: retention уже настроен (id: $EXISTING_RET), пропускаю" + continue + fi + + # Создать retention policy для проекта + RET_RESP=$(curl -sf -X POST -u "$AUTH" -H "Content-Type: application/json" \ + -d "{ + \"algorithm\": \"or\", + \"rules\": [{ + \"disabled\": false, + \"action\": \"retain\", + \"template\": \"latestPushedN\", + \"params\": {\"latestPushedN\": {{ harbor_retention_max_tags }}}, + \"tag_selectors\": [{\"kind\": \"doublestar\", \"decoration\": \"matches\", \"pattern\": \"**\"}], + \"scope_selectors\": { + \"repository\": [{\"kind\": \"doublestar\", \"decoration\": \"repoMatches\", \"pattern\": \"**\"}] + } + }], + \"trigger\": {\"kind\": \"Schedule\", \"settings\": {\"cron\": \"0 0 3 * * *\"}}, + \"scope\": {\"level\": \"project\", \"ref\": $PROJ_ID} + }" \ + "$BASE/api/v2.0/retentions" 2>/dev/null || echo "{}") + + RET_ID=$(echo "$RET_RESP" | jq '.id // empty' 2>/dev/null || echo "") + if [ -n "$RET_ID" ] && [ "$RET_ID" != "null" ]; then + echo " $PROJ_NAME (id: $PROJ_ID): retention policy #$RET_ID установлена" + else + echo " $PROJ_NAME: не удалось создать retention (возможно уже настроено)" + fi + done < /tmp/projects.json + + echo ">>> Retention policies настроены (запуск: ежедневно в 03:00 UTC)" +{% endif %} + + echo "" + echo ">>> Конфигурация Harbor завершена!" +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: harbor-configure + namespace: {{ harbor_namespace }} +spec: + ttlSecondsAfterFinished: 600 + backoffLimit: 3 + template: + spec: + restartPolicy: OnFailure + containers: + - name: configure + image: alpine:3.19 + command: ["/bin/sh", "/scripts/configure.sh"] + volumeMounts: + - name: scripts + mountPath: /scripts + volumes: + - name: scripts + configMap: + name: harbor-configure + defaultMode: 0755