feat: harbor integration — EnsureProject + per-namespace image path
- internal/harbor/client.go: новый пакет, EnsureProject (GET+POST idempotent)
- config.go: добавлены HarborUser/HarborPass (из HARBOR_USER/HARBOR_PASS env)
- builder.go: Projecter интерфейс, harborClient поле, ImageRef: {host}/{ns}/{func}:{tag}
EnsureProject вызывается перед каждым Build()
- function_controller.go: HarborClient поле, EnsureProject при создании k8s NS
- main.go: создание harbor.Client если HARBOR_USER+HARBOR_PASS заданы
- operator.yaml: REGISTRY_HOST=pearlharbor..., HARBOR_USER=admin, v0.1.24
- hack/create-registry-secret.sh: переписан для Harbor (HARBOR_USER/HARBOR_PASS)
Смена registry — только через REGISTRY_HOST в ConfigMap, больше нигде.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// Изменено: 2026-03-08
|
// Изменено: 2026-03-11
|
||||||
// FunctionReconciler — основной контроллер оператора.
|
// FunctionReconciler — основной контроллер оператора.
|
||||||
// Следит за CRD Function и управляет lifecycle функции:
|
// Следит за CRD Function и управляет lifecycle функции:
|
||||||
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
|
||||||
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/harbor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FunctionReconciler reconciles a Function object
|
// FunctionReconciler reconciles a Function object
|
||||||
@@ -32,8 +33,9 @@ type FunctionReconciler struct {
|
|||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
Builder *builder.Builder
|
Builder *builder.Builder
|
||||||
RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций)
|
RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций)
|
||||||
OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-*
|
OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-*
|
||||||
|
HarborClient *harbor.Client // nil — Harbor не используется, EnsureProject пропускается
|
||||||
}
|
}
|
||||||
|
|
||||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete
|
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete
|
||||||
@@ -183,6 +185,13 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
|
|||||||
if err := r.Create(ctx, ns); err != nil {
|
if err := r.Create(ctx, ns); err != nil {
|
||||||
return ctrl.Result{}, fmt.Errorf("create function namespace: %w", err)
|
return ctrl.Result{}, fmt.Errorf("create function namespace: %w", err)
|
||||||
}
|
}
|
||||||
|
// Создаём Harbor-проект для namespace сразу при создании k8s NS (best-effort).
|
||||||
|
// Если не удалось — EnsureProject повторит вызов внутри Build().
|
||||||
|
if r.HarborClient != nil {
|
||||||
|
if err := r.HarborClient.EnsureProject(ctx, fn.Namespace); err != nil {
|
||||||
|
log.FromContext(ctx).Error(err, "harbor ensure project on ns create", "project", fn.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ctrl.Result{}, fmt.Errorf("get function namespace: %w", err)
|
return ctrl.Result{}, fmt.Errorf("get function namespace: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
# Изменено: 2026-03-07
|
# Изменено: 2026-03-11
|
||||||
# Деплой sless оператора в кластер.
|
# Деплой sless оператора в кластер.
|
||||||
# Состав:
|
# Состав:
|
||||||
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
|
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
|
||||||
# - Secret: секретные данные (S3 keys, postgres DSN, API token, docker auth)
|
# - Secret: секретные данные (S3 keys, postgres DSN, API token, Harbor pass)
|
||||||
# - Deployment: оператор naeel/sless-operator:v0.1.23 в namespace sless
|
# - Deployment: оператор naeel/sless-operator:v0.1.23 в namespace sless
|
||||||
# - Service: ClusterIP :9090 (REST API)
|
# - Service: ClusterIP :9090 (REST API)
|
||||||
# - Ingress: sless-api.kube5s.ru → :9090 (внешний доступ с TLS)
|
# - Ingress: sless-api.kube5s.ru → :9090 (внешний доступ с TLS)
|
||||||
#
|
#
|
||||||
# Перед применением:
|
# Чтобы сменить registry — менять только REGISTRY_HOST в ConfigMap.
|
||||||
# kubectl create secret generic sless-operator-secret \
|
# Чтобы сменить Harbor-аккаунт — менять HARBOR_USER в ConfigMap + HARBOR_PASS в Secret.
|
||||||
# --namespace=sless \
|
|
||||||
# --from-literal=POSTGRES_DSN="..." \
|
|
||||||
# --from-literal=S3_ACCESS_KEY="..." \
|
|
||||||
# --from-literal=S3_SECRET_KEY="..." \
|
|
||||||
# --from-literal=SLESS_API_TOKEN="change-me" \
|
|
||||||
# --dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
#
|
#
|
||||||
# Применение: kubectl apply -f deployments/k8s/operator.yaml
|
# Применение: kubectl apply -f deployments/k8s/operator.yaml
|
||||||
---
|
---
|
||||||
@@ -27,8 +21,13 @@ data:
|
|||||||
S3_ENDPOINT: "s3.msk-1.ngcloud.ru"
|
S3_ENDPOINT: "s3.msk-1.ngcloud.ru"
|
||||||
S3_BUCKET: "sless-functions"
|
S3_BUCKET: "sless-functions"
|
||||||
S3_USE_SSL: "true"
|
S3_USE_SSL: "true"
|
||||||
REGISTRY_HOST: "naeel"
|
# REGISTRY_HOST — единственное место, где прописан адрес registry.
|
||||||
|
# Чтобы сменить реестр — менять только здесь.
|
||||||
|
# Harbor: pearlharbor.registryk8s.services.ngcloud.ru
|
||||||
|
# DockerHub (legacy): naeel
|
||||||
|
REGISTRY_HOST: "pearlharbor.registryk8s.services.ngcloud.ru"
|
||||||
REGISTRY_SECRET: "sless-registry-auth"
|
REGISTRY_SECRET: "sless-registry-auth"
|
||||||
|
HARBOR_USER: "admin"
|
||||||
API_PORT: "9090"
|
API_PORT: "9090"
|
||||||
INGRESS_HOST: "fn.kube5s.ru"
|
INGRESS_HOST: "fn.kube5s.ru"
|
||||||
# EXTERNAL_URL — если задан, URL функции = EXTERNAL_URL/fn/{namespace}/{name}
|
# EXTERNAL_URL — если задан, URL функции = EXTERNAL_URL/fn/{namespace}/{name}
|
||||||
@@ -41,13 +40,16 @@ data:
|
|||||||
# S3_ACCESS_KEY — ключ доступа к S3/Ceph
|
# S3_ACCESS_KEY — ключ доступа к S3/Ceph
|
||||||
# S3_SECRET_KEY — секретный ключ S3/Ceph
|
# S3_SECRET_KEY — секретный ключ S3/Ceph
|
||||||
# SLESS_API_TOKEN — токен аутентификации API
|
# SLESS_API_TOKEN — токен аутентификации API
|
||||||
|
# HARBOR_PASS — пароль Harbor API (для EnsureProject, не для kaniko push)
|
||||||
#
|
#
|
||||||
# Пример создания:
|
# Пример создания:
|
||||||
# kubectl create secret generic sless-operator-secret -n sless \
|
# kubectl create secret generic sless-operator-secret -n sless \
|
||||||
# --from-literal=POSTGRES_DSN="postgres://sless:PASSWORD@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \
|
# --from-literal=POSTGRES_DSN="postgres://sless:PASSWORD@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \
|
||||||
# --from-literal=S3_ACCESS_KEY="ACCESS_KEY" \
|
# --from-literal=S3_ACCESS_KEY="ACCESS_KEY" \
|
||||||
# --from-literal=S3_SECRET_KEY="SECRET_KEY" \
|
# --from-literal=S3_SECRET_KEY="SECRET_KEY" \
|
||||||
# --from-literal=SLESS_API_TOKEN="your-token-here"
|
# --from-literal=SLESS_API_TOKEN="your-token-here" \
|
||||||
|
# --from-literal=HARBOR_PASS="harbor-admin-password"
|
||||||
|
# kubectl apply -f hack/create-registry-secret.sh # docker-креды для kaniko
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
@@ -70,7 +72,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: operator
|
- name: operator
|
||||||
# При обновлении версии оператора — менять тег здесь (не latest!)
|
# При обновлении версии оператора — менять тег здесь (не latest!)
|
||||||
image: naeel/sless-operator:v0.1.23
|
image: naeel/sless-operator:v0.1.24
|
||||||
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Изменено: 2026-03-07
|
# Изменено: 2026-03-11
|
||||||
# Создаёт k8s Secret sless-registry-auth с DockerHub кредами для kaniko.
|
# Создаёт k8s Secret sless-registry-auth с кредами Harbor для kaniko.
|
||||||
# Kaniko монтирует этот secret как /kaniko/.docker/config.json для push образов.
|
# Kaniko монтирует этот secret как /kaniko/.docker/config.json для push образов.
|
||||||
|
# Отдельно от HARBOR_PASS в operator-secret: этот secret — для kaniko (push),
|
||||||
|
# HARBOR_PASS — для Harbor API (создание проектов через EnsureProject).
|
||||||
#
|
#
|
||||||
# Использование:
|
# Использование:
|
||||||
# DOCKER_HUB_USER=naeel DOCKER_HUB_TOKEN=<token> ./hack/create-registry-secret.sh
|
# HARBOR_USER=admin HARBOR_PASS=<password> REGISTRY=pearlharbor.registryk8s.services.ngcloud.ru \
|
||||||
|
# ./hack/create-registry-secret.sh
|
||||||
#
|
#
|
||||||
# Требуется: kubectl, авторизованный context с доступом к namespace sless.
|
# Требуется: kubectl, авторизованный context с доступом к namespace sless.
|
||||||
|
|
||||||
@@ -12,21 +15,22 @@ set -euo pipefail
|
|||||||
|
|
||||||
NAMESPACE="sless"
|
NAMESPACE="sless"
|
||||||
SECRET_NAME="sless-registry-auth"
|
SECRET_NAME="sless-registry-auth"
|
||||||
|
REGISTRY="${REGISTRY:-pearlharbor.registryk8s.services.ngcloud.ru}"
|
||||||
|
|
||||||
if [[ -z "${DOCKER_HUB_USER:-}" ]]; then
|
if [[ -z "${HARBOR_USER:-}" ]]; then
|
||||||
echo "ERROR: DOCKER_HUB_USER env var is required"
|
echo "ERROR: HARBOR_USER env var is required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ -z "${DOCKER_HUB_TOKEN:-}" ]]; then
|
if [[ -z "${HARBOR_PASS:-}" ]]; then
|
||||||
echo "ERROR: DOCKER_HUB_TOKEN env var is required (DockerHub access token, not password)"
|
echo "ERROR: HARBOR_PASS env var is required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kubectl create secret docker-registry "${SECRET_NAME}" \
|
kubectl create secret docker-registry "${SECRET_NAME}" \
|
||||||
--docker-username="${DOCKER_HUB_USER}" \
|
--docker-username="${HARBOR_USER}" \
|
||||||
--docker-password="${DOCKER_HUB_TOKEN}" \
|
--docker-password="${HARBOR_PASS}" \
|
||||||
--docker-server="https://index.docker.io/v1/" \
|
--docker-server="${REGISTRY}" \
|
||||||
--namespace="${NAMESPACE}" \
|
--namespace="${NAMESPACE}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
echo "OK: secret '${SECRET_NAME}' applied in namespace '${NAMESPACE}'"
|
echo "OK: secret '${SECRET_NAME}' applied in namespace '${NAMESPACE}' (server: ${REGISTRY})"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Изменено: 2026-03-07
|
// Изменено: 2026-03-11
|
||||||
// Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции.
|
// Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции.
|
||||||
// Почему kaniko, а не docker-in-docker (DinD):
|
// Почему kaniko, а не docker-in-docker (DinD):
|
||||||
// kaniko не требует privileged контейнер, что безопаснее в managed кластере.
|
// kaniko не требует privileged контейнер, что безопаснее в managed кластере.
|
||||||
// kaniko читает контекст сборки из S3 напрямую.
|
// kaniko читает контекст сборки из S3 напрямую.
|
||||||
// Workflow: FunctionController вызывает Build → Job запускается → образ пушится в registry.
|
// Workflow: FunctionController вызывает Build → EnsureProject (Harbor) → Job запускается → образ пушится в registry.
|
||||||
|
|
||||||
package builder
|
package builder
|
||||||
|
|
||||||
@@ -20,17 +20,24 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Projecter — опциональный интерфейс для реестров с поддержкой проектов (Harbor).
|
||||||
|
// Если nil — вызов EnsureProject пропускается (Docker Hub, другие реестры без проектов).
|
||||||
|
type Projecter interface {
|
||||||
|
EnsureProject(ctx context.Context, name string) error
|
||||||
|
}
|
||||||
|
|
||||||
// Builder — управляет сборкой Docker образов через kaniko Jobs в k8s.
|
// Builder — управляет сборкой Docker образов через kaniko Jobs в k8s.
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
client client.Client
|
client client.Client
|
||||||
builderImage string // образ kaniko
|
builderImage string // образ kaniko
|
||||||
registryHost string // куда пушим образ
|
registryHost string // куда пушим образ (Harbor: host; DockerHub: user/org)
|
||||||
registrySecret string // имя k8s Secret с docker-кредами для kaniko
|
registrySecret string // имя k8s Secret с docker-кредами для kaniko
|
||||||
s3Endpoint string // откуда kaniko берёт код
|
s3Endpoint string // откуда kaniko берёт код
|
||||||
s3AccessKey string
|
s3AccessKey string
|
||||||
s3SecretKey string
|
s3SecretKey string
|
||||||
s3Bucket string
|
s3Bucket string
|
||||||
namespace string // namespace где запускаем build Job'ы
|
namespace string // namespace где запускаем build Job'ы
|
||||||
|
harborClient Projecter // nil если Harbor не используется
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config — параметры для создания Builder'а.
|
// Config — параметры для создания Builder'а.
|
||||||
@@ -43,6 +50,7 @@ type Config struct {
|
|||||||
S3SecretKey string
|
S3SecretKey string
|
||||||
S3Bucket string
|
S3Bucket string
|
||||||
Namespace string
|
Namespace string
|
||||||
|
HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается
|
||||||
}
|
}
|
||||||
|
|
||||||
// New создаёт новый Builder.
|
// New создаёт новый Builder.
|
||||||
@@ -57,21 +65,34 @@ func New(c client.Client, cfg Config) *Builder {
|
|||||||
s3SecretKey: cfg.S3SecretKey,
|
s3SecretKey: cfg.S3SecretKey,
|
||||||
s3Bucket: cfg.S3Bucket,
|
s3Bucket: cfg.S3Bucket,
|
||||||
namespace: cfg.Namespace,
|
namespace: cfg.Namespace,
|
||||||
|
harborClient: cfg.HarborClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageRef возвращает полный путь к образу в registry для данной функции и версии.
|
// ImageRef возвращает полный путь к образу в registry для данной функции и версии.
|
||||||
// Тег = первые 12 символов sha256(s3Key): уникален per build, не меняется при
|
// Тег = первые 12 символов sha256(s3Key): уникален per build, не меняется при
|
||||||
// повторном reconcile с тем же s3Key, не требует хаков с :latest.
|
// повторном reconcile с тем же s3Key, не требует хаков с :latest.
|
||||||
|
// Формат: {registryHost}/{namespace}/{funcName}:{tag}
|
||||||
|
//
|
||||||
|
// Harbor: pearlharbor.registryk8s.services.ngcloud.ru/{ns}/{func}:{tag}
|
||||||
|
//
|
||||||
|
// Чтобы сменить реестр — достаточно изменить REGISTRY_HOST в одном месте (ConfigMap).
|
||||||
func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
|
func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
|
||||||
h := sha256.Sum256([]byte(s3Key))
|
h := sha256.Sum256([]byte(s3Key))
|
||||||
tag := fmt.Sprintf("%x", h[:6]) // 12 hex-символов
|
tag := fmt.Sprintf("%x", h[:6]) // 12 hex-символов
|
||||||
return fmt.Sprintf("%s/sless-%s-%s:%s", b.registryHost, namespace, funcName, tag)
|
return fmt.Sprintf("%s/%s/%s:%s", b.registryHost, namespace, funcName, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build запускает kaniko Job для сборки образа функции.
|
// Build запускает kaniko Job для сборки образа функции.
|
||||||
// Возвращает имя Job'а чтобы контроллер мог следить за его статусом.
|
// Возвращает имя Job'а чтобы контроллер мог следить за его статусом.
|
||||||
func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) {
|
func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) {
|
||||||
|
// Идемпотентно создаём Harbor-проект перед push — kaniko сам его не создаёт.
|
||||||
|
// Пропускаем если HarborClient не задан (Docker Hub и другие реестры без проектов).
|
||||||
|
if b.harborClient != nil {
|
||||||
|
if err := b.harborClient.EnsureProject(ctx, namespace); err != nil {
|
||||||
|
return "", fmt.Errorf("harbor ensure project %q: %w", namespace, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
imageRef := b.ImageRef(namespace, funcName, s3Key)
|
imageRef := b.ImageRef(namespace, funcName, s3Key)
|
||||||
jobName := fmt.Sprintf("build-%s-%s", funcName, time.Now().Format("20060102150405"))
|
jobName := fmt.Sprintf("build-%s-%s", funcName, time.Now().Format("20060102150405"))
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,14 @@ type Config struct {
|
|||||||
// Поле сохранено для совместимости конфигурации.
|
// Поле сохранено для совместимости конфигурации.
|
||||||
// Аутентификация происходит в middleware/auth.go через validateJWT().
|
// Аутентификация происходит в middleware/auth.go через validateJWT().
|
||||||
APIToken string
|
APIToken string
|
||||||
|
|
||||||
|
// Harbor — креды для Harbor API (создание проектов до push образов).
|
||||||
|
// REGISTRY_HOST уже содержит адрес реестра — дублировать не нужно.
|
||||||
|
// Если не заданы, EnsureProject пропускается (только DockerHub/без проектов).
|
||||||
|
HarborUser string
|
||||||
|
HarborPass string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load читает конфиг из env переменных.
|
|
||||||
// Возвращает ошибку если обязательные переменные не заданы.
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
APIPort: 8080,
|
APIPort: 8080,
|
||||||
@@ -128,5 +132,10 @@ func Load() (*Config, error) {
|
|||||||
// Не required: auth middleware использует validateJWT, а не статический токен.
|
// Не required: auth middleware использует validateJWT, а не статический токен.
|
||||||
cfg.APIToken = os.Getenv("SLESS_API_TOKEN")
|
cfg.APIToken = os.Getenv("SLESS_API_TOKEN")
|
||||||
|
|
||||||
|
// Harbor API креды — опциональны.
|
||||||
|
// Если не заданы, EnsureProject пропускается (push работает через docker-кред в kaniko).
|
||||||
|
cfg.HarborUser = os.Getenv("HARBOR_USER")
|
||||||
|
cfg.HarborPass = os.Getenv("HARBOR_PASS")
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Изменено: 2026-03-11
|
||||||
|
// Harbor API клиент — управление проектами (create/ensure) перед push образов.
|
||||||
|
// Почему здесь: kaniko не создаёт Harbor-проект сам, нужен явный вызов API.
|
||||||
|
// Все операции идемпотентны - повторный вызов EnsureProject безопасен.
|
||||||
|
package harbor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client — минимальный HTTP-клиент для Harbor API v2.
|
||||||
|
// Используется только для создания проектов; пуш образов делает kaniko напрямую.
|
||||||
|
type Client struct {
|
||||||
|
baseURL string // https://{registryHost}
|
||||||
|
user string
|
||||||
|
password string
|
||||||
|
http *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New создаёт Harbor клиент.
|
||||||
|
// registryHost — хост реестра без схемы (e.g. "pearlharbor.registryk8s.services.ngcloud.ru").
|
||||||
|
func New(registryHost, user, password string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: "https://" + registryHost,
|
||||||
|
user: user,
|
||||||
|
password: password,
|
||||||
|
http: &http.Client{Timeout: 15 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureProject идемпотентно создаёт Harbor проект с именем name (private).
|
||||||
|
// GET /api/v2.0/projects?name={name} → если не найден → POST /api/v2.0/projects.
|
||||||
|
// Возвращает nil если проект уже существовал или был успешно создан.
|
||||||
|
func (c *Client) EnsureProject(ctx context.Context, name string) error {
|
||||||
|
// Harbor GET projects возвращает список проектов по фильтру name=
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||||
|
fmt.Sprintf("%s/api/v2.0/projects?name=%s", c.baseURL, name), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("harbor: build GET request: %w", err)
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(c.user, c.password)
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("harbor: GET project %q: %w", name, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
return fmt.Errorf("harbor: unauthorized — проверьте HARBOR_USER и HARBOR_PASS")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projects []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil {
|
||||||
|
return fmt.Errorf("harbor: decode GET response: %w", err)
|
||||||
|
}
|
||||||
|
for _, p := range projects {
|
||||||
|
if p.Name == name {
|
||||||
|
return nil // проект уже существует
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проект не найден — создаём
|
||||||
|
body, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"project_name": name,
|
||||||
|
"public": false,
|
||||||
|
})
|
||||||
|
req, err = http.NewRequestWithContext(ctx, http.MethodPost,
|
||||||
|
c.baseURL+"/api/v2.0/projects", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("harbor: build POST request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.SetBasicAuth(c.user, c.password)
|
||||||
|
|
||||||
|
resp2, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("harbor: POST project %q: %w", name, err)
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
// 201 Created — успешно; 409 Conflict — гонка (кто-то создал параллельно), оба ок
|
||||||
|
if resp2.StatusCode != http.StatusCreated && resp2.StatusCode != http.StatusConflict {
|
||||||
|
return fmt.Errorf("harbor: POST project %q: unexpected status %d", name, resp2.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api/handler"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api/handler"
|
||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
|
||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/config"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/config"
|
||||||
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/harbor"
|
||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres"
|
||||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3"
|
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3"
|
||||||
//+kubebuilder:scaffold:imports
|
//+kubebuilder:scaffold:imports
|
||||||
@@ -118,6 +119,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Builder использует k8s client из manager'а
|
// Builder использует k8s client из manager'а
|
||||||
|
// harborClient — nil если HARBOR_USER/HARBOR_PASS не заданы (EnsureProject пропускается).
|
||||||
|
var harborClient *harbor.Client
|
||||||
|
if cfg.HarborUser != "" && cfg.HarborPass != "" {
|
||||||
|
harborClient = harbor.New(cfg.RegistryHost, cfg.HarborUser, cfg.HarborPass)
|
||||||
|
log.Info("harbor client configured", "host", cfg.RegistryHost)
|
||||||
|
} else {
|
||||||
|
log.Info("harbor client disabled (HARBOR_USER/HARBOR_PASS not set)")
|
||||||
|
}
|
||||||
|
|
||||||
bldr := builder.New(mgr.GetClient(), builder.Config{
|
bldr := builder.New(mgr.GetClient(), builder.Config{
|
||||||
BuilderImage: cfg.BuilderImage,
|
BuilderImage: cfg.BuilderImage,
|
||||||
RegistryHost: cfg.RegistryHost,
|
RegistryHost: cfg.RegistryHost,
|
||||||
@@ -127,6 +137,7 @@ func main() {
|
|||||||
S3SecretKey: cfg.S3SecretKey,
|
S3SecretKey: cfg.S3SecretKey,
|
||||||
S3Bucket: cfg.S3Bucket,
|
S3Bucket: cfg.S3Bucket,
|
||||||
Namespace: "sless",
|
Namespace: "sless",
|
||||||
|
HarborClient: harborClient,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err = (&controllers.FunctionReconciler{
|
if err = (&controllers.FunctionReconciler{
|
||||||
@@ -135,6 +146,7 @@ func main() {
|
|||||||
Builder: bldr,
|
Builder: bldr,
|
||||||
RegistrySecret: cfg.RegistrySecret,
|
RegistrySecret: cfg.RegistrySecret,
|
||||||
OperatorNamespace: "sless",
|
OperatorNamespace: "sless",
|
||||||
|
HarborClient: harborClient,
|
||||||
}).SetupWithManager(mgr); err != nil {
|
}).SetupWithManager(mgr); err != nil {
|
||||||
log.Error("unable to create controller", "controller", "Function", "err", err)
|
log.Error("unable to create controller", "controller", "Function", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user