feat: REST API (gorilla/mux + slog) + trigger controller + main.go wiring

- 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
This commit is contained in:
“Naeel”
2026-03-07 08:46:10 +04:00
parent 5f25f61948
commit 43e7d0ea48
21 changed files with 1720 additions and 112 deletions
+155
View File
@@ -0,0 +1,155 @@
// Изменено: 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 }