fix: imagePullSecrets, SLESS_ENTRYPOINT, registry secret propagation
- function_controller: добавить RegistrySecret + OperatorNamespace, копировать sless-registry-auth в sless-fn-<ns>, выставлять imagePullSecrets в Deployment, также обновлять imagePullSecrets при reconcile - functionjob_controller: fnEnvVars включает SLESS_ENTRYPOINT, runner читает его - server.js + server.py: читать SLESS_ENTRYPOINT вместо hardcoded handler.js/py - rbac.yaml: добавить права на secrets - operator.yaml: v0.1.8 - main.go: передать RegistrySecret + OperatorNamespace в FunctionReconciler
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Изменено: 2026-03-07
|
||||
// Изменено: 2026-03-08
|
||||
// FunctionReconciler — основной контроллер оператора.
|
||||
// Следит за CRD Function и управляет lifecycle функции:
|
||||
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
||||
@@ -28,8 +28,10 @@ import (
|
||||
// FunctionReconciler reconciles a Function object
|
||||
type FunctionReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Builder *builder.Builder
|
||||
Scheme *runtime.Scheme
|
||||
Builder *builder.Builder
|
||||
RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций)
|
||||
OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-*
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -38,6 +40,7 @@ type FunctionReconciler struct {
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;create
|
||||
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
||||
|
||||
// Reconcile — главный цикл управления Function.
|
||||
@@ -183,6 +186,15 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
|
||||
}
|
||||
}
|
||||
|
||||
// Обеспечиваем наличие registry pull-секрета в namespace функций.
|
||||
// Без него kubelet не сможет pull-нуть private образ из Harbor.
|
||||
if r.RegistrySecret != "" && r.OperatorNamespace != "" {
|
||||
if err := r.ensureRegistrySecret(ctx, deployNS); err != nil {
|
||||
// Не фатальная ошибка — логируем, но продолжаем
|
||||
log.FromContext(ctx).Error(err, "failed to ensure registry secret", "ns", deployNS)
|
||||
}
|
||||
}
|
||||
|
||||
desired := r.buildDeployment(fn, deployNS)
|
||||
existing := &appsv1.Deployment{}
|
||||
err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, existing)
|
||||
@@ -196,8 +208,9 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
|
||||
return ctrl.Result{}, fmt.Errorf("get deployment: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем образ если изменился (новая сборка)
|
||||
// Обновляем образ и imagePullSecrets если изменились (новая сборка или смена конфига)
|
||||
existing.Spec.Template.Spec.Containers[0].Image = fn.Status.ImageRef
|
||||
existing.Spec.Template.Spec.ImagePullSecrets = desired.Spec.Template.Spec.ImagePullSecrets
|
||||
if err := r.Update(ctx, existing); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update deployment: %w", err)
|
||||
}
|
||||
@@ -207,7 +220,11 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
|
||||
// buildDeployment формирует Deployment манифест для функции.
|
||||
func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespace string) *appsv1.Deployment {
|
||||
replicas := int32(1)
|
||||
envVars := []corev1.EnvVar{}
|
||||
envVars := []corev1.EnvVar{
|
||||
// SLESS_ENTRYPOINT сообщает server.py/server.js какой файл и функцию загружать.
|
||||
// Формат: "module-name.funcName" (например: handler-http.handle)
|
||||
{Name: "SLESS_ENTRYPOINT", Value: fn.Spec.Entrypoint},
|
||||
}
|
||||
for k, v := range fn.Spec.Env {
|
||||
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
@@ -236,12 +253,51 @@ func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespa
|
||||
},
|
||||
},
|
||||
},
|
||||
ImagePullSecrets: func() []corev1.LocalObjectReference {
|
||||
if r.RegistrySecret != "" {
|
||||
return []corev1.LocalObjectReference{{Name: r.RegistrySecret}}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ensureRegistrySecret копирует pull-секрет из namespace оператора в namespace функций.
|
||||
// Вызывается при каждом reconcile — если секрет уже есть, ничего не делает.
|
||||
func (r *FunctionReconciler) ensureRegistrySecret(ctx context.Context, targetNS string) error {
|
||||
// Проверяем что секрет уже есть в целевом namespace
|
||||
existing := &corev1.Secret{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: r.RegistrySecret, Namespace: targetNS}, existing); err == nil {
|
||||
return nil // уже есть
|
||||
} else if !errors.IsNotFound(err) {
|
||||
return fmt.Errorf("check secret: %w", err)
|
||||
}
|
||||
|
||||
// Копируем из namespace оператора
|
||||
src := &corev1.Secret{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: r.RegistrySecret, Namespace: r.OperatorNamespace}, src); err != nil {
|
||||
return fmt.Errorf("get source secret from %s: %w", r.OperatorNamespace, err)
|
||||
}
|
||||
|
||||
copy := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.RegistrySecret,
|
||||
Namespace: targetNS,
|
||||
},
|
||||
Type: src.Type,
|
||||
Data: src.Data,
|
||||
}
|
||||
if err := r.Create(ctx, copy); err != nil {
|
||||
if !errors.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("create secret in %s: %w", targetNS, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDeletion обрабатывает удаление Function: удаляет Deployment и убирает finalizer.
|
||||
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
deployNS := "sless-fn-" + fn.Namespace
|
||||
|
||||
@@ -200,15 +200,20 @@ func (r *FunctionJobReconciler) syncJobStatus(ctx context.Context, fj *slessv1al
|
||||
}
|
||||
|
||||
// runtimeRunnerCommand возвращает CMD для запуска одноразового runner вместо HTTP-сервера.
|
||||
// runner читает env SLESS_EVENT, вызывает handle(event) один раз и завершается.
|
||||
// runner читает env SLESS_EVENT и SLESS_ENTRYPOINT, вызывает handle(event) один раз и завершается.
|
||||
func runtimeRunnerCommand(runtime string) []string {
|
||||
switch runtime {
|
||||
case "nodejs20":
|
||||
// inline runner — не требует отдельного файла в образе
|
||||
// inline runner — не требует отдельного файла в образе.
|
||||
// SLESS_ENTRYPOINT="module.func": module=имя файла, func=экспортируемая функция
|
||||
return []string{"node", "-e", `
|
||||
const h = require('/app/function/handler.js');
|
||||
const ep = process.env.SLESS_ENTRYPOINT || 'handler.handle';
|
||||
const dot = ep.lastIndexOf('.');
|
||||
const mod = ep.slice(0, dot >= 0 ? dot : ep.length);
|
||||
const fn = dot >= 0 ? ep.slice(dot + 1) : 'handle';
|
||||
const h = require('/app/function/' + mod);
|
||||
const event = JSON.parse(process.env.SLESS_EVENT || '{}');
|
||||
Promise.resolve(h.handle(event)).then(r => {
|
||||
Promise.resolve(h[fn](event)).then(r => {
|
||||
console.log(JSON.stringify(r));
|
||||
process.exit(0);
|
||||
}).catch(e => {
|
||||
@@ -218,19 +223,26 @@ Promise.resolve(h.handle(event)).then(r => {
|
||||
default: // python3.11
|
||||
return []string{"python3", "-c", `
|
||||
import os, json, importlib.util
|
||||
spec = importlib.util.spec_from_file_location("handler", "/app/function/handler.py")
|
||||
ep = os.environ.get("SLESS_ENTRYPOINT", "handler.handle")
|
||||
dot = ep.rfind(".")
|
||||
mod_name = ep[:dot] if dot >= 0 else ep
|
||||
fn_name = ep[dot+1:] if dot >= 0 else "handle"
|
||||
spec = importlib.util.spec_from_file_location(mod_name, "/app/function/" + mod_name + ".py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
event = json.loads(os.environ.get("SLESS_EVENT", "{}"))
|
||||
result = mod.handle(event)
|
||||
result = getattr(mod, fn_name)(event)
|
||||
print(json.dumps(result))
|
||||
`}
|
||||
}
|
||||
}
|
||||
|
||||
// fnEnvVars преобразует env vars из FunctionSpec в k8s EnvVar slice.
|
||||
// Включает SLESS_ENTRYPOINT чтобы runner.py/runner.js знал какую функцию вызывать.
|
||||
func fnEnvVars(fn *slessv1alpha1.Function) []corev1.EnvVar {
|
||||
var result []corev1.EnvVar
|
||||
result := []corev1.EnvVar{
|
||||
{Name: "SLESS_ENTRYPOINT", Value: fn.Spec.Entrypoint},
|
||||
}
|
||||
for k, v := range fn.Spec.Env {
|
||||
result = append(result, corev1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user