feat(v0.1.59): in-cluster registry:2 — insecure HTTP mode, ImageExists error handling
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
Reference in New Issue
Block a user