@@ -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 {