From c033adec1161fd733b090ea9661b602d263bafa5 Mon Sep 17 00:00:00 2001 From: Naeel Date: Mon, 23 Mar 2026 06:41:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(v0.1.59):=20in-cluster=20registry:2=20?= =?UTF-8?q?=E2=80=94=20insecure=20HTTP=20mode,=20ImageExists=20error=20han?= =?UTF-8?q?dling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/function_controller.go | 7 +- controllers/functionjob_controller.go | 7 +- controllers/service_controller.go | 7 +- internal/builder/builder.go | 170 ++++++++++++++++---------- internal/config/config.go | 10 ++ main.go | 21 ++-- 6 files changed, 143 insertions(+), 79 deletions(-) diff --git a/controllers/function_controller.go b/controllers/function_controller.go index 5b4f7a1..8ae52b3 100644 --- a/controllers/function_controller.go +++ b/controllers/function_controller.go @@ -115,7 +115,12 @@ func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.F // Проверяем: образ с этим тегом уже существует в registry? // Если да — пропускаем kaniko, сразу переходим в Ready. - if r.Builder.ImageExists(ctx, imageRef) { + // Если registry недоступен — requeue, не запускаем сборку (kaniko тоже упадёт). + exists, err := r.Builder.ImageExists(ctx, imageRef) + if err != nil { + return ctrl.Result{RequeueAfter: 10 * time.Second}, fmt.Errorf("check image exists: %w", err) + } + if exists { logger := log.FromContext(ctx) logger.Info("image already exists in registry, skipping build", "imageRef", imageRef) diff --git a/controllers/functionjob_controller.go b/controllers/functionjob_controller.go index ee2d11e..ab096df 100644 --- a/controllers/functionjob_controller.go +++ b/controllers/functionjob_controller.go @@ -124,7 +124,12 @@ func (r *FunctionJobReconciler) startJobBuild(ctx context.Context, fj *slessv1al imageRef := r.Builder.ImageRef(r.OperatorNamespace, fj.Name, fj.Spec.S3Key) // Проверяем: образ с этим тегом уже существует в registry? - if r.Builder.ImageExists(ctx, imageRef) { + // Если registry недоступен — requeue, не запускаем сборку. + exists, err := r.Builder.ImageExists(ctx, imageRef) + if err != nil { + return ctrl.Result{RequeueAfter: 10 * time.Second}, fmt.Errorf("check image exists: %w", err) + } + if exists { logger.Info("image already exists in registry, skipping build", "imageRef", imageRef) if fj.Annotations == nil { diff --git a/controllers/service_controller.go b/controllers/service_controller.go index d9a77b3..f174009 100644 --- a/controllers/service_controller.go +++ b/controllers/service_controller.go @@ -109,7 +109,12 @@ func (r *ServiceReconciler) startServiceBuild(ctx context.Context, svc *slessv1a // Проверяем: образ с этим тегом уже существует в registry? // Если да — пропускаем kaniko, сразу переходим в Ready с известным imageRef. - if r.Builder.ImageExists(ctx, imageRef) { + // Если registry недоступен — requeue, не запускаем сборку (kaniko тоже упадёт). + exists, err := r.Builder.ImageExists(ctx, imageRef) + if err != nil { + return ctrl.Result{RequeueAfter: 10 * time.Second}, fmt.Errorf("check image exists: %w", err) + } + if exists { logger := log.FromContext(ctx) logger.Info("image already exists in registry, skipping build", "imageRef", imageRef) diff --git a/internal/builder/builder.go b/internal/builder/builder.go index aea9c07..ee8758a 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -34,47 +34,55 @@ type Projecter interface { // Builder — управляет сборкой Docker образов через kaniko Jobs в k8s. type Builder struct { - client client.Client - builderImage string // образ kaniko - registryHost string // куда пушим образ (DockerHub: "naeel"; Harbor: "host") - registryProject string // проект/org внутри registry (Harbor: project; DockerHub: пусто) - registrySecret string // имя k8s Secret с docker-кредами для kaniko - s3Endpoint string // откуда kaniko берёт код - s3AccessKey string - s3SecretKey string - s3Bucket string - namespace string // namespace где запускаем build Job'ы - harborClient Projecter // nil если Harbor не используется + client client.Client + builderImage string // образ kaniko + registryHost string // куда пушим образ (DockerHub: "naeel"; Harbor: "host") + registryProject string // проект/org внутри registry (Harbor: project; DockerHub: пусто) + registrySecret string // имя k8s Secret с docker-кредами для kaniko + registryInsecure bool // true = in-cluster HTTP registry, без TLS и авторизации + s3Endpoint string // откуда kaniko берёт код + s3AccessKey string + s3SecretKey string + s3Bucket string + namespace string // namespace где запускаем build Job'ы + harborClient Projecter // nil если Harbor не используется } // Config — параметры для создания Builder'а. type Config struct { - BuilderImage string - RegistryHost string - RegistryProject string // пусто = DockerHub-режим (2 уровня); задан = project-режим (3 уровня) - RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов - S3Endpoint string - S3AccessKey string - S3SecretKey string - S3Bucket string - Namespace string - HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается + BuilderImage string + RegistryHost string + RegistryProject string // пусто = DockerHub-режим (2 уровня); задан = project-режим (3 уровня) + RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов + RegistryInsecure bool // true = HTTP registry (in-cluster), kaniko получает --insecure + S3Endpoint string + S3AccessKey string + S3SecretKey string + S3Bucket string + Namespace string + HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается } // New создаёт новый Builder. func New(c client.Client, cfg Config) *Builder { + // In-cluster HTTP registry не требует docker credentials — монтировать Secret не нужно. + registrySecret := cfg.RegistrySecret + if cfg.RegistryInsecure { + registrySecret = "" + } return &Builder{ - client: c, - builderImage: cfg.BuilderImage, - registryHost: cfg.RegistryHost, - registryProject: cfg.RegistryProject, - registrySecret: cfg.RegistrySecret, - s3Endpoint: cfg.S3Endpoint, - s3AccessKey: cfg.S3AccessKey, - s3SecretKey: cfg.S3SecretKey, - s3Bucket: cfg.S3Bucket, - namespace: cfg.Namespace, - harborClient: cfg.HarborClient, + client: c, + builderImage: cfg.BuilderImage, + registryHost: cfg.RegistryHost, + registryProject: cfg.RegistryProject, + registrySecret: registrySecret, + registryInsecure: cfg.RegistryInsecure, + s3Endpoint: cfg.S3Endpoint, + s3AccessKey: cfg.S3AccessKey, + s3SecretKey: cfg.S3SecretKey, + s3Bucket: cfg.S3Bucket, + namespace: cfg.Namespace, + harborClient: cfg.HarborClient, } } @@ -110,74 +118,94 @@ func (b *Builder) ImageRef(namespace, funcName, s3Key string) string { } // ImageExists проверяет наличие образа в Docker Registry v2 по тегу (без pull). -// Использует анонимный bearer-token для публичных репо (DockerHub). -// Возвращает true если образ с таким тегом уже запушен — сборка не нужна. +// Возвращает (true, nil) если образ есть, (false, nil) если нет, +// (false, err) если registry недоступен — контроллер должен requeue, не запускать сборку. // -// Почему анонимный токен, а не credentials: -// -// DockerHub выдаёт pull-token без авторизации для публичных репо через -// GET /token?service=registry.docker.io&scope=repository:{repo}:pull -// Это стандартный Docker Registry v2 auth flow (RFC 7235). -func (b *Builder) ImageExists(ctx context.Context, imageRef string) bool { - // imageRef вида: "naeel/slessffd1-pg-search:47cab27ada70" - // или "host/project/func:tag" — разбираем по последнему ":" +// Два режима: +// - insecure (in-cluster registry:2): HTTP, без TLS, без авторизации +// - secure (DockerHub): HTTPS, анонимный bearer-token для публичных репо +func (b *Builder) ImageExists(ctx context.Context, imageRef string) (bool, error) { colonIdx := strings.LastIndex(imageRef, ":") if colonIdx < 0 { - return false + return false, nil } repoFull := imageRef[:colonIdx] tag := imageRef[colonIdx+1:] - // Определяем registry host и repo path. - // DockerHub: "naeel/sless-ff-pg" → registry = index.docker.io, repo = "naeel/sless-ff-pg" - // Приватный: "harbor.host/proj/func" → registry = "harbor.host", repo = "proj/func" + if b.registryInsecure { + // In-cluster HTTP registry — без авторизации и TLS. + // repoFull вида "host:port/path" → берём host до первого / + slashIdx := strings.Index(repoFull, "/") + if slashIdx < 0 { + return false, nil + } + registryHost := repoFull[:slashIdx] + repo := repoFull[slashIdx+1:] + + httpClient := &http.Client{Timeout: 5 * time.Second} + manifestURL := fmt.Sprintf("http://%s/v2/%s/manifests/%s", registryHost, repo, tag) + req, err := http.NewRequestWithContext(ctx, http.MethodHead, manifestURL, nil) + if err != nil { + return false, err + } + req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") + resp, err := httpClient.Do(req) + if err != nil { + // Registry недоступен — не притворяемся что образа нет, requeue + return false, fmt.Errorf("registry unavailable at %s: %w", registryHost, err) + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + default: + return false, fmt.Errorf("unexpected registry response: %d", resp.StatusCode) + } + } + + // Secure registry (DockerHub или HTTPS). + // DockerHub: "naeel/sless-ff-pg" → registry = index.docker.io + // Приватный: "harbor.host/proj/func" → registry = harbor.host registryHost := "index.docker.io" repo := repoFull if parts := strings.SplitN(repoFull, "/", 3); len(parts) == 3 { - // host/project/name — кастомный registry registryHost = parts[0] repo = parts[1] + "/" + parts[2] } - // Получаем анонимный/публичный bearer-token для pull доступа к репо. - // DockerHub: https://auth.docker.io/token?service=registry.docker.io&scope=repository:{repo}:pull - // Для приватных registry этот шаг вернёт 401 → ImageExists вернёт false → пойдём строить. tokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repo) if registryHost != "index.docker.io" { - // Для не-DockerHub registry: пробуем без токена (Harbor с allow anon push) - // Если 401 — false, пусть builder разберётся. tokenURL = "" } httpClient := &http.Client{Timeout: 5 * time.Second} - var bearerToken string if tokenURL != "" { req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil) if err != nil { - return false + return false, nil } resp, err := httpClient.Do(req) if err != nil || resp.StatusCode != http.StatusOK { - return false + return false, nil } defer resp.Body.Close() var tkResp struct { Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&tkResp); err != nil { - return false + return false, nil } bearerToken = tkResp.Token } - // HEAD /v2/{repo}/manifests/{tag} — проверяем наличие тега без скачивания слоёв. manifestURL := fmt.Sprintf("https://%s/v2/%s/manifests/%s", registryHost, repo, tag) req, err := http.NewRequestWithContext(ctx, http.MethodHead, manifestURL, nil) if err != nil { - return false + return false, nil } - // Docker Registry v2 требует Accept header для манифестов. req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") if bearerToken != "" { req.Header.Set("Authorization", "Bearer "+bearerToken) @@ -185,10 +213,10 @@ func (b *Builder) ImageExists(ctx context.Context, imageRef string) bool { resp, err := httpClient.Do(req) if err != nil { - return false + return false, nil } defer resp.Body.Close() - return resp.StatusCode == http.StatusOK + return resp.StatusCode == http.StatusOK, nil } // Build запускает kaniko Job для сборки образа функции. @@ -230,11 +258,7 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) { Name: "kaniko", Image: b.builderImage, - Args: []string{ - "--context=" + s3ContextURL, - "--destination=" + imageRef, - // --no-cache не поддерживается этой версией kaniko; кэш отключён по умолчанию - }, + Args: b.kanikoArgs(s3ContextURL, imageRef), Env: []corev1.EnvVar{ {Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey}, {Name: "AWS_SECRET_ACCESS_KEY", Value: b.s3SecretKey}, @@ -298,6 +322,20 @@ func (b *Builder) Cleanup(ctx context.Context, jobName string) error { return b.client.Delete(ctx, job, &client.DeleteOptions{PropagationPolicy: &propagation}) } +// kanikoArgs строит аргументы для kaniko контейнера. +// Добавляет --insecure если registry работает по HTTP (in-cluster). +func (b *Builder) kanikoArgs(s3ContextURL, imageRef string) []string { + args := []string{ + "--context=" + s3ContextURL, + "--destination=" + imageRef, + } + if b.registryInsecure { + // --insecure: push в HTTP registry без TLS (in-cluster registry:2) + args = append(args, "--insecure") + } + return args +} + // dockerConfigVolumes возвращает Volume с docker-кредами если RegistrySecret задан. // Ключ .dockerconfigjson монтируется как config.json — стандартное имя для kaniko. func (b *Builder) dockerConfigVolumes() []corev1.Volume { diff --git a/internal/config/config.go b/internal/config/config.go index b88c543..b74782a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,6 +40,11 @@ type Config struct { // Создаётся через hack/create-registry-secret.sh RegistrySecret string + // RegistryInsecure — использовать HTTP (без TLS) для registry. + // Нужно для in-cluster registry:2 где нет сертификата. + // Включается через REGISTRY_INSECURE=true. + RegistryInsecure bool + // BuilderImage — образ для сборки функций (kaniko или buildah) BuilderImage string @@ -128,6 +133,11 @@ func Load() (*Config, error) { cfg.RegistrySecret = "sless-registry-auth" } + // REGISTRY_INSECURE=true — использовать HTTP вместо HTTPS для registry (in-cluster registry:2) + if os.Getenv("REGISTRY_INSECURE") == "true" { + cfg.RegistryInsecure = true + } + // Опциональные параметры с дефолтами if v := os.Getenv("BUILDER_IMAGE"); v != "" { cfg.BuilderImage = v diff --git a/main.go b/main.go index 61f104a..4550f3b 100644 --- a/main.go +++ b/main.go @@ -131,16 +131,17 @@ func main() { } bldr := builder.New(mgr.GetClient(), builder.Config{ - BuilderImage: cfg.BuilderImage, - RegistryHost: cfg.RegistryHost, - RegistryProject: cfg.RegistryProject, - RegistrySecret: cfg.RegistrySecret, - S3Endpoint: cfg.S3Endpoint, - S3AccessKey: cfg.S3AccessKey, - S3SecretKey: cfg.S3SecretKey, - S3Bucket: cfg.S3Bucket, - Namespace: "sless", - HarborClient: harborProjecter, + BuilderImage: cfg.BuilderImage, + RegistryHost: cfg.RegistryHost, + RegistryProject: cfg.RegistryProject, + RegistrySecret: cfg.RegistrySecret, + RegistryInsecure: cfg.RegistryInsecure, + S3Endpoint: cfg.S3Endpoint, + S3AccessKey: cfg.S3AccessKey, + S3SecretKey: cfg.S3SecretKey, + S3Bucket: cfg.S3Bucket, + Namespace: "sless", + HarborClient: harborProjecter, }) if err = (&controllers.FunctionReconciler{