feat(v0.1.58): ImageExists cache hit, timing analysis, in-cluster registry plan

This commit is contained in:
Naeel
2026-03-23 06:22:25 +03:00
parent 7023e0e6fc
commit 9edd43edc5
20 changed files with 596 additions and 515 deletions
+107 -19
View File
@@ -1,16 +1,22 @@
// Изменено: 2026-03-11
// Изменено: 2026-03-22
// Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции.
// Почему kaniko, а не docker-in-docker (DinD):
// kaniko не требует privileged контейнер, что безопаснее в managed кластере.
// kaniko читает контекст сборки из S3 напрямую.
// Workflow: FunctionController вызывает Build → EnsureProject (Harbor) → Job запускается → образ пушится в registry.
// ImageExists: перед сборкой проверяем наличие образа по тегу в registry.
// Если образ с таким тегом (= hash кода) уже существует — сборка пропускается.
// Это даёт идемпотентность destroy+apply: если код не менялся, deploy мгновенный.
package builder
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
batchv1 "k8s.io/api/batch/v1"
@@ -33,12 +39,12 @@ type Builder struct {
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 не используется
s3Endpoint string // откуда kaniko берёт код
s3AccessKey string
s3SecretKey string
s3Bucket string
namespace string // namespace где запускаем build Job'ы
harborClient Projecter // nil если Harbor не используется
}
// Config — параметры для создания Builder'а.
@@ -47,12 +53,12 @@ type Config struct {
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 пропускается
S3Endpoint string
S3AccessKey string
S3SecretKey string
S3Bucket string
Namespace string
HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается
}
// New создаёт новый Builder.
@@ -63,12 +69,12 @@ func New(c client.Client, cfg Config) *Builder {
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,
s3Endpoint: cfg.S3Endpoint,
s3AccessKey: cfg.S3AccessKey,
s3SecretKey: cfg.S3SecretKey,
s3Bucket: cfg.S3Bucket,
namespace: cfg.Namespace,
harborClient: cfg.HarborClient,
}
}
@@ -103,6 +109,88 @@ func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
return fmt.Sprintf("%s/%s-%s:%s", b.registryHost, nsPrefix, funcName, tag)
}
// ImageExists проверяет наличие образа в Docker Registry v2 по тегу (без pull).
// Использует анонимный bearer-token для публичных репо (DockerHub).
// Возвращает true если образ с таким тегом уже запушен — сборка не нужна.
//
// Почему анонимный токен, а не 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" — разбираем по последнему ":"
colonIdx := strings.LastIndex(imageRef, ":")
if colonIdx < 0 {
return false
}
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"
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
}
resp, err := httpClient.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
return false
}
defer resp.Body.Close()
var tkResp struct {
Token string `json:"token"`
}
if err := json.NewDecoder(resp.Body).Decode(&tkResp); err != nil {
return false
}
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
}
// Docker Registry v2 требует Accept header для манифестов.
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
if bearerToken != "" {
req.Header.Set("Authorization", "Bearer "+bearerToken)
}
resp, err := httpClient.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// Build запускает kaniko Job для сборки образа функции.
// Возвращает имя Job'а чтобы контроллер мог следить за его статусом.
func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) {