- controllers/trigger_controller.go: полная реализация, HTTP->Ingress+Service, Cron->CronJob
- internal/config: добавлены IngressHost, APIToken
- internal/api/router.go: gorilla/mux роутер /v1/namespaces/{ns}/...
- internal/api/handler/: functions, triggers, invocations, base handler с slog
- internal/api/middleware/: auth (Bearer token) + logging (slog)
- main.go: запуск operator + HTTP сервера параллельно
- go.sum: добавлен gorilla/mux v1.8.1
156 lines
5.7 KiB
Go
156 lines
5.7 KiB
Go
// Изменено: 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 // куда пушим образ
|
||
s3Endpoint string // откуда kaniko берёт код
|
||
s3AccessKey string
|
||
s3SecretKey string
|
||
s3Bucket string
|
||
namespace string // namespace где запускаем build Job'ы
|
||
}
|
||
|
||
// Config — параметры для создания Builder'а.
|
||
type Config struct {
|
||
BuilderImage string
|
||
RegistryHost string
|
||
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,
|
||
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,
|
||
"--skip-tls-verify", // для внутреннего registry без TLS
|
||
"--cache=true", // кешируем слои для ускорения повторных сборок
|
||
},
|
||
Env: []corev1.EnvVar{
|
||
{Name: "AWS_ACCESS_KEY_ID", Value: b.s3AccessKey},
|
||
{Name: "AWS_SECRET_ACCESS_KEY", Value: b.s3SecretKey},
|
||
{Name: "S3_ENDPOINT", Value: b.s3Endpoint},
|
||
// Kaniko использует AWS SDK совместимый с S3 — задаём кастомный endpoint
|
||
{Name: "AWS_REGION", Value: "us-east-1"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
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})
|
||
}
|
||
|
||
func int32Ptr(i int32) *int32 { return &i }
|