Files
sless/internal/builder/builder.go
T
“Naeel” 8bae4ca69c fix: kaniko S3 endpoint — добавить https:// схему и S3_FORCE_PATH_STYLE=true
Kaniko AWS SDK требует полный URL с протоколом.
Ceph не поддерживает virtual-hosted style — нужен path style.
2026-03-07 09:39:53 +04:00

194 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Изменено: 2026-03-07
// Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции.
// Почему kaniko, а не docker-in-docker (DinD):
// kaniko не требует privileged контейнер, что безопаснее в managed кластере.
// kaniko читает контекст сборки из S3 напрямую.
// Workflow: FunctionController вызывает Build → Job запускается → образ пушится в registry.
package builder
import (
"context"
"fmt"
"time"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Builder — управляет сборкой Docker образов через kaniko Jobs в k8s.
type Builder struct {
client client.Client
builderImage string // образ kaniko
registryHost string // куда пушим образ
registrySecret string // имя k8s Secret с docker-кредами для kaniko
s3Endpoint string // откуда kaniko берёт код
s3AccessKey string
s3SecretKey string
s3Bucket string
namespace string // namespace где запускаем build Job'ы
}
// Config — параметры для создания Builder'а.
type Config struct {
BuilderImage string
RegistryHost string
RegistrySecret string // имя k8s Secret с .dockerconfigjson для пуша образов
S3Endpoint string
S3AccessKey string
S3SecretKey string
S3Bucket string
Namespace string
}
// New создаёт новый Builder.
func New(c client.Client, cfg Config) *Builder {
return &Builder{
client: c,
builderImage: cfg.BuilderImage,
registryHost: cfg.RegistryHost,
registrySecret: cfg.RegistrySecret,
s3Endpoint: cfg.S3Endpoint,
s3AccessKey: cfg.S3AccessKey,
s3SecretKey: cfg.S3SecretKey,
s3Bucket: cfg.S3Bucket,
namespace: cfg.Namespace,
}
}
// ImageRef возвращает полный путь к образу в registry для данной функции и версии.
func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
// Используем s3Key как уникальный тег чтобы разные версии не перезаписывали друг друга
return fmt.Sprintf("%s/sless-%s-%s:latest", b.registryHost, namespace, funcName)
}
// Build запускает kaniko Job для сборки образа функции.
// Возвращает имя Job'а чтобы контроллер мог следить за его статусом.
func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) {
imageRef := b.ImageRef(namespace, funcName, s3Key)
jobName := fmt.Sprintf("build-%s-%s", funcName, time.Now().Format("20060102150405"))
// kaniko читает Dockerfile из context архива в S3
// --context=s3://bucket/key — kaniko поддерживает S3 как источник контекста
s3ContextURL := fmt.Sprintf("s3://%s/%s", b.s3Bucket, s3Key)
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
Namespace: b.namespace,
Labels: map[string]string{
"app": "sless-builder",
"function-name": funcName,
"function-ns": namespace,
},
},
Spec: batchv1.JobSpec{
// Не повторяем при ошибке — контроллер сам перезапустит reconcile
BackoffLimit: int32Ptr(0),
Completions: int32Ptr(1),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "kaniko",
Image: b.builderImage,
Args: []string{
"--context=" + s3ContextURL,
"--destination=" + imageRef,
"--cache=true", // кешируем слои для ускорения повторных сборок
},
Env: []corev1.EnvVar{
{Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey},
{Name: "AWS_SECRET_ACCESS_KEY", Value: b.s3SecretKey},
// Kaniko AWS SDK требует схему в endpoint (https://) — без неё URL строится без протокола
{Name: "S3_ENDPOINT", Value: "https://" + b.s3Endpoint},
// Ceph не поддерживает virtual-hosted style (bucket.endpoint) — только path style
{Name: "S3_FORCE_PATH_STYLE", Value: "true"},
{Name: "AWS_REGION", Value: "us-east-1"},
},
// Монтируем docker-креды из k8s Secret — kaniko читает авторизацию из /kaniko/.docker/config.json
VolumeMounts: b.dockerConfigMounts(),
},
},
Volumes: b.dockerConfigVolumes(),
},
},
},
}
if err := b.client.Create(ctx, job); err != nil {
return "", fmt.Errorf("create build job: %w", err)
}
return jobName, nil
}
// JobStatus проверяет статус build Job'а.
// Возвращает: "running", "succeeded", "failed"
func (b *Builder) JobStatus(ctx context.Context, jobName string) (string, error) {
job := &batchv1.Job{}
if err := b.client.Get(ctx, client.ObjectKey{Name: jobName, Namespace: b.namespace}, job); err != nil {
if errors.IsNotFound(err) {
return "failed", nil
}
return "", fmt.Errorf("get build job: %w", err)
}
if job.Status.Succeeded > 0 {
return "succeeded", nil
}
if job.Status.Failed > 0 {
return "failed", nil
}
return "running", nil
}
// Cleanup удаляет завершённый build Job из k8s.
// Вызывается после того как контроллер зафиксировал результат сборки.
func (b *Builder) Cleanup(ctx context.Context, jobName string) error {
job := &batchv1.Job{}
if err := b.client.Get(ctx, client.ObjectKey{Name: jobName, Namespace: b.namespace}, job); err != nil {
if errors.IsNotFound(err) {
return nil // уже удалён
}
return fmt.Errorf("get build job for cleanup: %w", err)
}
propagation := metav1.DeletePropagationForeground
return b.client.Delete(ctx, job, &client.DeleteOptions{PropagationPolicy: &propagation})
}
// dockerConfigVolumes возвращает Volume с docker-кредами если RegistrySecret задан.
// Ключ .dockerconfigjson монтируется как config.json — стандартное имя для kaniko.
func (b *Builder) dockerConfigVolumes() []corev1.Volume {
if b.registrySecret == "" {
return nil
}
return []corev1.Volume{
{
Name: "docker-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: b.registrySecret,
Items: []corev1.KeyToPath{
{Key: ".dockerconfigjson", Path: "config.json"},
},
},
},
},
}
}
// dockerConfigMounts возвращает VolumeMount для docker-кредов если RegistrySecret задан.
func (b *Builder) dockerConfigMounts() []corev1.VolumeMount {
if b.registrySecret == "" {
return nil
}
return []corev1.VolumeMount{
{Name: "docker-config", MountPath: "/kaniko/.docker"},
}
}
func int32Ptr(i int32) *int32 { return &i }