feat(v0.1.59): in-cluster registry:2 — insecure HTTP mode, ImageExists error handling

This commit is contained in:
Naeel
2026-03-23 06:41:30 +03:00
parent 9edd43edc5
commit c033adec11
6 changed files with 143 additions and 79 deletions
+6 -1
View File
@@ -115,7 +115,12 @@ func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.F
// Проверяем: образ с этим тегом уже существует в registry? // Проверяем: образ с этим тегом уже существует в registry?
// Если да — пропускаем kaniko, сразу переходим в Ready. // Если да — пропускаем 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 := log.FromContext(ctx)
logger.Info("image already exists in registry, skipping build", "imageRef", imageRef) logger.Info("image already exists in registry, skipping build", "imageRef", imageRef)
+6 -1
View File
@@ -124,7 +124,12 @@ func (r *FunctionJobReconciler) startJobBuild(ctx context.Context, fj *slessv1al
imageRef := r.Builder.ImageRef(r.OperatorNamespace, fj.Name, fj.Spec.S3Key) imageRef := r.Builder.ImageRef(r.OperatorNamespace, fj.Name, fj.Spec.S3Key)
// Проверяем: образ с этим тегом уже существует в registry? // Проверяем: образ с этим тегом уже существует в 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) logger.Info("image already exists in registry, skipping build", "imageRef", imageRef)
if fj.Annotations == nil { if fj.Annotations == nil {
+6 -1
View File
@@ -109,7 +109,12 @@ func (r *ServiceReconciler) startServiceBuild(ctx context.Context, svc *slessv1a
// Проверяем: образ с этим тегом уже существует в registry? // Проверяем: образ с этим тегом уже существует в registry?
// Если да — пропускаем kaniko, сразу переходим в Ready с известным imageRef. // Если да — пропускаем 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 := log.FromContext(ctx)
logger.Info("image already exists in registry, skipping build", "imageRef", imageRef) logger.Info("image already exists in registry, skipping build", "imageRef", imageRef)
+104 -66
View File
@@ -34,47 +34,55 @@ type Projecter interface {
// 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 // куда пушим образ (DockerHub: "naeel"; Harbor: "host") registryHost string // куда пушим образ (DockerHub: "naeel"; Harbor: "host")
registryProject string // проект/org внутри registry (Harbor: project; DockerHub: пусто) registryProject string // проект/org внутри registry (Harbor: project; DockerHub: пусто)
registrySecret string // имя k8s Secret с docker-кредами для kaniko registrySecret string // имя k8s Secret с docker-кредами для kaniko
s3Endpoint string // откуда kaniko берёт код registryInsecure bool // true = in-cluster HTTP registry, без TLS и авторизации
s3AccessKey string s3Endpoint string // откуда kaniko берёт код
s3SecretKey string s3AccessKey string
s3Bucket string s3SecretKey string
namespace string // namespace где запускаем build Job'ы s3Bucket string
harborClient Projecter // nil если Harbor не используется namespace string // namespace где запускаем build Job'ы
harborClient Projecter // nil если Harbor не используется
} }
// Config — параметры для создания Builder'а. // Config — параметры для создания Builder'а.
type Config struct { type Config struct {
BuilderImage string BuilderImage string
RegistryHost string RegistryHost string
RegistryProject string // пусто = DockerHub-режим (2 уровня); задан = project-режим (3 уровня) RegistryProject string // пусто = DockerHub-режим (2 уровня); задан = project-режим (3 уровня)
RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов
S3Endpoint string RegistryInsecure bool // true = HTTP registry (in-cluster), kaniko получает --insecure
S3AccessKey string S3Endpoint string
S3SecretKey string S3AccessKey string
S3Bucket string S3SecretKey string
Namespace string S3Bucket string
HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается Namespace string
HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается
} }
// New создаёт новый Builder. // New создаёт новый Builder.
func New(c client.Client, cfg Config) *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{ return &Builder{
client: c, client: c,
builderImage: cfg.BuilderImage, builderImage: cfg.BuilderImage,
registryHost: cfg.RegistryHost, registryHost: cfg.RegistryHost,
registryProject: cfg.RegistryProject, registryProject: cfg.RegistryProject,
registrySecret: cfg.RegistrySecret, registrySecret: registrySecret,
s3Endpoint: cfg.S3Endpoint, registryInsecure: cfg.RegistryInsecure,
s3AccessKey: cfg.S3AccessKey, s3Endpoint: cfg.S3Endpoint,
s3SecretKey: cfg.S3SecretKey, s3AccessKey: cfg.S3AccessKey,
s3Bucket: cfg.S3Bucket, s3SecretKey: cfg.S3SecretKey,
namespace: cfg.Namespace, s3Bucket: cfg.S3Bucket,
harborClient: cfg.HarborClient, namespace: cfg.Namespace,
harborClient: cfg.HarborClient,
} }
} }
@@ -110,74 +118,94 @@ func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
} }
// ImageExists проверяет наличие образа в Docker Registry v2 по тегу (без pull). // ImageExists проверяет наличие образа в Docker Registry v2 по тегу (без pull).
// Использует анонимный bearer-token для публичных репо (DockerHub). // Возвращает (true, nil) если образ есть, (false, nil) если нет,
// Возвращает true если образ с таким тегом уже запушен — сборка не нужна. // (false, err) если registry недоступен — контроллер должен requeue, не запускать сборку.
// //
// Почему анонимный токен, а не credentials: // Два режима:
// // - insecure (in-cluster registry:2): HTTP, без TLS, без авторизации
// DockerHub выдаёт pull-token без авторизации для публичных репо через // - secure (DockerHub): HTTPS, анонимный bearer-token для публичных репо
// GET /token?service=registry.docker.io&scope=repository:{repo}:pull func (b *Builder) ImageExists(ctx context.Context, imageRef string) (bool, error) {
// Это стандартный 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" — разбираем по последнему ":"
colonIdx := strings.LastIndex(imageRef, ":") colonIdx := strings.LastIndex(imageRef, ":")
if colonIdx < 0 { if colonIdx < 0 {
return false return false, nil
} }
repoFull := imageRef[:colonIdx] repoFull := imageRef[:colonIdx]
tag := imageRef[colonIdx+1:] tag := imageRef[colonIdx+1:]
// Определяем registry host и repo path. if b.registryInsecure {
// DockerHub: "naeel/sless-ff-pg" → registry = index.docker.io, repo = "naeel/sless-ff-pg" // In-cluster HTTP registry — без авторизации и TLS.
// Приватный: "harbor.host/proj/func" → registry = "harbor.host", repo = "proj/func" // 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" registryHost := "index.docker.io"
repo := repoFull repo := repoFull
if parts := strings.SplitN(repoFull, "/", 3); len(parts) == 3 { if parts := strings.SplitN(repoFull, "/", 3); len(parts) == 3 {
// host/project/name — кастомный registry
registryHost = parts[0] registryHost = parts[0]
repo = parts[1] + "/" + parts[2] 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) tokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repo)
if registryHost != "index.docker.io" { if registryHost != "index.docker.io" {
// Для не-DockerHub registry: пробуем без токена (Harbor с allow anon push)
// Если 401 — false, пусть builder разберётся.
tokenURL = "" tokenURL = ""
} }
httpClient := &http.Client{Timeout: 5 * time.Second} httpClient := &http.Client{Timeout: 5 * time.Second}
var bearerToken string var bearerToken string
if tokenURL != "" { if tokenURL != "" {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
if err != nil { if err != nil {
return false return false, nil
} }
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
return false return false, nil
} }
defer resp.Body.Close() defer resp.Body.Close()
var tkResp struct { var tkResp struct {
Token string `json:"token"` Token string `json:"token"`
} }
if err := json.NewDecoder(resp.Body).Decode(&tkResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&tkResp); err != nil {
return false return false, nil
} }
bearerToken = tkResp.Token bearerToken = tkResp.Token
} }
// HEAD /v2/{repo}/manifests/{tag} — проверяем наличие тега без скачивания слоёв.
manifestURL := fmt.Sprintf("https://%s/v2/%s/manifests/%s", registryHost, repo, tag) manifestURL := fmt.Sprintf("https://%s/v2/%s/manifests/%s", registryHost, repo, tag)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, manifestURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodHead, manifestURL, nil)
if err != 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") req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
if bearerToken != "" { if bearerToken != "" {
req.Header.Set("Authorization", "Bearer "+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) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return false return false, nil
} }
defer resp.Body.Close() defer resp.Body.Close()
return resp.StatusCode == http.StatusOK return resp.StatusCode == http.StatusOK, nil
} }
// Build запускает kaniko Job для сборки образа функции. // Build запускает kaniko Job для сборки образа функции.
@@ -230,11 +258,7 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string)
{ {
Name: "kaniko", Name: "kaniko",
Image: b.builderImage, Image: b.builderImage,
Args: []string{ Args: b.kanikoArgs(s3ContextURL, imageRef),
"--context=" + s3ContextURL,
"--destination=" + imageRef,
// --no-cache не поддерживается этой версией kaniko; кэш отключён по умолчанию
},
Env: []corev1.EnvVar{ Env: []corev1.EnvVar{
{Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey}, {Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey},
{Name: "AWS_SECRET_ACCESS_KEY", Value: b.s3SecretKey}, {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}) 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 задан. // dockerConfigVolumes возвращает Volume с docker-кредами если RegistrySecret задан.
// Ключ .dockerconfigjson монтируется как config.json — стандартное имя для kaniko. // Ключ .dockerconfigjson монтируется как config.json — стандартное имя для kaniko.
func (b *Builder) dockerConfigVolumes() []corev1.Volume { func (b *Builder) dockerConfigVolumes() []corev1.Volume {
+10
View File
@@ -40,6 +40,11 @@ type Config struct {
// Создаётся через hack/create-registry-secret.sh // Создаётся через hack/create-registry-secret.sh
RegistrySecret string RegistrySecret string
// RegistryInsecure — использовать HTTP (без TLS) для registry.
// Нужно для in-cluster registry:2 где нет сертификата.
// Включается через REGISTRY_INSECURE=true.
RegistryInsecure bool
// BuilderImage — образ для сборки функций (kaniko или buildah) // BuilderImage — образ для сборки функций (kaniko или buildah)
BuilderImage string BuilderImage string
@@ -128,6 +133,11 @@ func Load() (*Config, error) {
cfg.RegistrySecret = "sless-registry-auth" 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 != "" { if v := os.Getenv("BUILDER_IMAGE"); v != "" {
cfg.BuilderImage = v cfg.BuilderImage = v
+11 -10
View File
@@ -131,16 +131,17 @@ func main() {
} }
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,
RegistryProject: cfg.RegistryProject, RegistryProject: cfg.RegistryProject,
RegistrySecret: cfg.RegistrySecret, RegistrySecret: cfg.RegistrySecret,
S3Endpoint: cfg.S3Endpoint, RegistryInsecure: cfg.RegistryInsecure,
S3AccessKey: cfg.S3AccessKey, S3Endpoint: cfg.S3Endpoint,
S3SecretKey: cfg.S3SecretKey, S3AccessKey: cfg.S3AccessKey,
S3Bucket: cfg.S3Bucket, S3SecretKey: cfg.S3SecretKey,
Namespace: "sless", S3Bucket: cfg.S3Bucket,
HarborClient: harborProjecter, Namespace: "sless",
HarborClient: harborProjecter,
}) })
if err = (&controllers.FunctionReconciler{ if err = (&controllers.FunctionReconciler{