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 — основной контроллер оператора.
|
// FunctionReconciler — основной контроллер оператора.
|
||||||
// Следит за CRD Function и управляет lifecycle функции:
|
// Следит за CRD Function и управляет lifecycle функции:
|
||||||
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
||||||
@@ -28,8 +28,10 @@ import (
|
|||||||
// FunctionReconciler reconciles a Function object
|
// FunctionReconciler reconciles a Function object
|
||||||
type FunctionReconciler struct {
|
type FunctionReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
Builder *builder.Builder
|
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
|
//+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=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=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=namespaces,verbs=get;list;watch;create
|
||||||
|
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;create
|
||||||
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
||||||
|
|
||||||
// Reconcile — главный цикл управления Function.
|
// 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)
|
desired := r.buildDeployment(fn, deployNS)
|
||||||
existing := &appsv1.Deployment{}
|
existing := &appsv1.Deployment{}
|
||||||
err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, existing)
|
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)
|
return ctrl.Result{}, fmt.Errorf("get deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем образ если изменился (новая сборка)
|
// Обновляем образ и imagePullSecrets если изменились (новая сборка или смена конфига)
|
||||||
existing.Spec.Template.Spec.Containers[0].Image = fn.Status.ImageRef
|
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 {
|
if err := r.Update(ctx, existing); err != nil {
|
||||||
return ctrl.Result{}, fmt.Errorf("update deployment: %w", err)
|
return ctrl.Result{}, fmt.Errorf("update deployment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -207,7 +220,11 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
|
|||||||
// buildDeployment формирует Deployment манифест для функции.
|
// buildDeployment формирует Deployment манифест для функции.
|
||||||
func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespace string) *appsv1.Deployment {
|
func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespace string) *appsv1.Deployment {
|
||||||
replicas := int32(1)
|
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 {
|
for k, v := range fn.Spec.Env {
|
||||||
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
|
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.
|
// handleDeletion обрабатывает удаление Function: удаляет Deployment и убирает finalizer.
|
||||||
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||||
deployNS := "sless-fn-" + fn.Namespace
|
deployNS := "sless-fn-" + fn.Namespace
|
||||||
|
|||||||
@@ -200,15 +200,20 @@ func (r *FunctionJobReconciler) syncJobStatus(ctx context.Context, fj *slessv1al
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runtimeRunnerCommand возвращает CMD для запуска одноразового runner вместо HTTP-сервера.
|
// runtimeRunnerCommand возвращает CMD для запуска одноразового runner вместо HTTP-сервера.
|
||||||
// runner читает env SLESS_EVENT, вызывает handle(event) один раз и завершается.
|
// runner читает env SLESS_EVENT и SLESS_ENTRYPOINT, вызывает handle(event) один раз и завершается.
|
||||||
func runtimeRunnerCommand(runtime string) []string {
|
func runtimeRunnerCommand(runtime string) []string {
|
||||||
switch runtime {
|
switch runtime {
|
||||||
case "nodejs20":
|
case "nodejs20":
|
||||||
// inline runner — не требует отдельного файла в образе
|
// inline runner — не требует отдельного файла в образе.
|
||||||
|
// SLESS_ENTRYPOINT="module.func": module=имя файла, func=экспортируемая функция
|
||||||
return []string{"node", "-e", `
|
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 || '{}');
|
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));
|
console.log(JSON.stringify(r));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
@@ -218,19 +223,26 @@ Promise.resolve(h.handle(event)).then(r => {
|
|||||||
default: // python3.11
|
default: // python3.11
|
||||||
return []string{"python3", "-c", `
|
return []string{"python3", "-c", `
|
||||||
import os, json, importlib.util
|
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)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(mod)
|
spec.loader.exec_module(mod)
|
||||||
event = json.loads(os.environ.get("SLESS_EVENT", "{}"))
|
event = json.loads(os.environ.get("SLESS_EVENT", "{}"))
|
||||||
result = mod.handle(event)
|
result = getattr(mod, fn_name)(event)
|
||||||
print(json.dumps(result))
|
print(json.dumps(result))
|
||||||
`}
|
`}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fnEnvVars преобразует env vars из FunctionSpec в k8s EnvVar slice.
|
// fnEnvVars преобразует env vars из FunctionSpec в k8s EnvVar slice.
|
||||||
|
// Включает SLESS_ENTRYPOINT чтобы runner.py/runner.js знал какую функцию вызывать.
|
||||||
func fnEnvVars(fn *slessv1alpha1.Function) []corev1.EnvVar {
|
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 {
|
for k, v := range fn.Spec.Env {
|
||||||
result = append(result, corev1.EnvVar{Name: k, Value: v})
|
result = append(result, corev1.EnvVar{Name: k, Value: v})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: operator
|
- name: operator
|
||||||
# При обновлении версии оператора — менять тег здесь (не latest!)
|
# При обновлении версии оператора — менять тег здесь (не latest!)
|
||||||
image: naeel/sless-operator:v0.1.6
|
image: naeel/sless-operator:v0.1.8
|
||||||
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ rules:
|
|||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["services", "namespaces"]
|
resources: ["services", "namespaces"]
|
||||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
# Secrets — pull-секрет копируется в sless-fn-* namespace при создании функций
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get", "create"]
|
||||||
- apiGroups: ["networking.k8s.io"]
|
- apiGroups: ["networking.k8s.io"]
|
||||||
resources: ["ingresses"]
|
resources: ["ingresses"]
|
||||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -34,7 +34,7 @@ resource "sless_job" "hello_run" {
|
|||||||
name = "hello-run"
|
name = "hello-run"
|
||||||
function = sless_function.hello_job.name
|
function = sless_function.hello_job.name
|
||||||
event_json = jsonencode({ numbers = [1, 2, 3, 4, 5] })
|
event_json = jsonencode({ numbers = [1, 2, 3, 4, 5] })
|
||||||
wait_timeout_sec = 120
|
wait_timeout_sec = 600
|
||||||
run_id = 1
|
run_id = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"version": 4,
|
||||||
|
"terraform_version": "1.12.2",
|
||||||
|
"serial": 22,
|
||||||
|
"lineage": "d12fc078-7aee-39d1-629d-358c3c135820",
|
||||||
|
"outputs": {
|
||||||
|
"trigger_url": {
|
||||||
|
"value": "https://sless-api.kube5s.ru/fn/default/hello-http",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"mode": "data",
|
||||||
|
"type": "archive_file",
|
||||||
|
"name": "handler_http",
|
||||||
|
"provider": "provider[\"registry.terraform.io/hashicorp/archive\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"exclude_symlink_directories": null,
|
||||||
|
"excludes": null,
|
||||||
|
"id": "16650367fe534ed2feb81be322fd4a9d80f77388",
|
||||||
|
"output_base64sha256": "/fY9RigPle6Yx9R9B9yEYmi5+jgL6PV3fmPlVK5ia3g=",
|
||||||
|
"output_base64sha512": "yKvMpTCZBynqV3LlE3wTZGMIS0EG0tY8LE+1iIETCuWGoc+bv4+Hlnve14bBWGOnvQHdqE84y4UDi8Pmnz1A2A==",
|
||||||
|
"output_file_mode": null,
|
||||||
|
"output_md5": "a74ae4ccb7337659439eacaf1831194d",
|
||||||
|
"output_path": "./handler-http.zip",
|
||||||
|
"output_sha": "16650367fe534ed2feb81be322fd4a9d80f77388",
|
||||||
|
"output_sha256": "fdf63d46280f95ee98c7d47d07dc846268b9fa380be8f5777e63e554ae626b78",
|
||||||
|
"output_sha512": "c8abcca530990729ea5772e5137c136463084b4106d2d63c2c4fb58881130ae586a1cf9bbf8f87967bded786c15863a7bd01dda84f38cb85038bc3e69f3d40d8",
|
||||||
|
"output_size": 409,
|
||||||
|
"source": [],
|
||||||
|
"source_content": null,
|
||||||
|
"source_content_filename": null,
|
||||||
|
"source_dir": null,
|
||||||
|
"source_file": "./code/handler-http.js",
|
||||||
|
"type": "zip"
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"identity_schema_version": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "data",
|
||||||
|
"type": "archive_file",
|
||||||
|
"name": "handler_job",
|
||||||
|
"provider": "provider[\"registry.terraform.io/hashicorp/archive\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"exclude_symlink_directories": null,
|
||||||
|
"excludes": null,
|
||||||
|
"id": "27327ec4d4ab6d5d6fdc3f82a5ab768c68146a66",
|
||||||
|
"output_base64sha256": "cI+JbO5lWW+3qo4DSDg5HXLQS7EXHze2W6vGCNk0iOI=",
|
||||||
|
"output_base64sha512": "4D841Y2OT5EVlRbi/NwGh7SHnRzKgZ6AF1Rx+AIspPKthrbsZi8oGY6qLYe/NJ4t46j1Y8WkZ4tJ6iBpL5g7uw==",
|
||||||
|
"output_file_mode": null,
|
||||||
|
"output_md5": "2c5c498c77ec002df7cbeac94f626af8",
|
||||||
|
"output_path": "./handler-job.zip",
|
||||||
|
"output_sha": "27327ec4d4ab6d5d6fdc3f82a5ab768c68146a66",
|
||||||
|
"output_sha256": "708f896cee65596fb7aa8e034838391d72d04bb1171f37b65babc608d93488e2",
|
||||||
|
"output_sha512": "e03f38d58d8e4f91159516e2fcdc0687b4879d1cca819e80175471f8022ca4f2ad86b6ec662f28198eaa2d87bf349e2de3a8f563c5a4678b49ea20692f983bbb",
|
||||||
|
"output_size": 489,
|
||||||
|
"source": [],
|
||||||
|
"source_content": null,
|
||||||
|
"source_content_filename": null,
|
||||||
|
"source_dir": null,
|
||||||
|
"source_file": "./code/handler-job.js",
|
||||||
|
"type": "zip"
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"identity_schema_version": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "sless_function",
|
||||||
|
"name": "hello_http",
|
||||||
|
"provider": "provider[\"terra.k8c.ru/naeel/sless\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"build_timeout_sec": 300,
|
||||||
|
"code_hash": "a74ae4ccb7337659439eacaf1831194d",
|
||||||
|
"code_path": "./handler-http.zip",
|
||||||
|
"entrypoint": "handler-http.handle",
|
||||||
|
"env_vars": null,
|
||||||
|
"image_ref": "pearlharbor.registryk8s.services.ngcloud.ru/sless/sless-default-hello-http:latest",
|
||||||
|
"memory_mb": 128,
|
||||||
|
"name": "hello-http",
|
||||||
|
"namespace": "default",
|
||||||
|
"phase": "Ready",
|
||||||
|
"runtime": "nodejs20",
|
||||||
|
"timeout_sec": 30
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"identity_schema_version": 0,
|
||||||
|
"dependencies": [
|
||||||
|
"data.archive_file.handler_http"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "sless_function",
|
||||||
|
"name": "hello_job",
|
||||||
|
"provider": "provider[\"terra.k8c.ru/naeel/sless\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"build_timeout_sec": 300,
|
||||||
|
"code_hash": "2c5c498c77ec002df7cbeac94f626af8",
|
||||||
|
"code_path": "./handler-job.zip",
|
||||||
|
"entrypoint": "handler-job.handle",
|
||||||
|
"env_vars": null,
|
||||||
|
"image_ref": "pearlharbor.registryk8s.services.ngcloud.ru/sless/sless-default-hello-job:latest",
|
||||||
|
"memory_mb": 128,
|
||||||
|
"name": "hello-job",
|
||||||
|
"namespace": "default",
|
||||||
|
"phase": "Ready",
|
||||||
|
"runtime": "nodejs20",
|
||||||
|
"timeout_sec": 30
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"identity_schema_version": 0,
|
||||||
|
"dependencies": [
|
||||||
|
"data.archive_file.handler_job"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "sless_trigger",
|
||||||
|
"name": "hello_http",
|
||||||
|
"provider": "provider[\"terra.k8c.ru/naeel/sless\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"active": true,
|
||||||
|
"enabled": true,
|
||||||
|
"function": "hello-http",
|
||||||
|
"name": "hello-http-trigger",
|
||||||
|
"namespace": "default",
|
||||||
|
"schedule": null,
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://sless-api.kube5s.ru/fn/default/hello-http"
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"identity_schema_version": 0,
|
||||||
|
"dependencies": [
|
||||||
|
"data.archive_file.handler_http",
|
||||||
|
"sless_function.hello_http"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"check_results": null
|
||||||
|
}
|
||||||
@@ -129,9 +129,11 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err = (&controllers.FunctionReconciler{
|
if err = (&controllers.FunctionReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
Builder: bldr,
|
Builder: bldr,
|
||||||
|
RegistrySecret: cfg.RegistrySecret,
|
||||||
|
OperatorNamespace: "sless",
|
||||||
}).SetupWithManager(mgr); err != nil {
|
}).SetupWithManager(mgr); err != nil {
|
||||||
log.Error("unable to create controller", "controller", "Function", "err", err)
|
log.Error("unable to create controller", "controller", "Function", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+14
-12
@@ -1,29 +1,31 @@
|
|||||||
// Изменено: 2026-03-07
|
// Изменено: 2026-03-08
|
||||||
// HTTP-обёртка для serverless функций на Node.js 20.
|
// HTTP-обёртка для serverless функций на Node.js 20.
|
||||||
// Загружает handler.js из /app/function/ и вызывает handle(event) на каждый запрос.
|
// Загружает модуль из SLESS_ENTRYPOINT или handler.js по умолчанию.
|
||||||
// Соглашение: handler.js должен экспортировать async функцию handle(event).
|
// Формат SLESS_ENTRYPOINT: "module-name.functionName" (например: handler-http.handle)
|
||||||
//
|
|
||||||
// Пример handler.js:
|
|
||||||
// exports.handle = async (event) => {
|
|
||||||
// return { message: `Hello, ${event.name || 'World'}!` };
|
|
||||||
// };
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const HANDLER_PATH = '/app/function/handler.js';
|
|
||||||
const PORT = 8080;
|
const PORT = 8080;
|
||||||
|
|
||||||
|
// Разбираем SLESS_ENTRYPOINT="module.func" → файл и имя функции.
|
||||||
|
// Fallback: handler.js + handle — для обратной совместимости.
|
||||||
|
const entrypoint = process.env.SLESS_ENTRYPOINT || 'handler.handle';
|
||||||
|
const dotIdx = entrypoint.lastIndexOf('.');
|
||||||
|
const moduleName = dotIdx >= 0 ? entrypoint.slice(0, dotIdx) : entrypoint;
|
||||||
|
const funcName = dotIdx >= 0 ? entrypoint.slice(dotIdx + 1) : 'handle';
|
||||||
|
const HANDLER_PATH = path.join('/app/function', moduleName);
|
||||||
|
|
||||||
// Загружаем модуль пользователя один раз при старте — не на каждый запрос
|
// Загружаем модуль пользователя один раз при старте — не на каждый запрос
|
||||||
let userHandle;
|
let userHandle;
|
||||||
try {
|
try {
|
||||||
const mod = require(HANDLER_PATH);
|
const mod = require(HANDLER_PATH);
|
||||||
if (typeof mod.handle !== 'function') {
|
if (typeof mod[funcName] !== 'function') {
|
||||||
throw new Error('handler.js must export a handle(event) function');
|
throw new Error(`${moduleName} must export a ${funcName}(event) function`);
|
||||||
}
|
}
|
||||||
userHandle = mod.handle;
|
userHandle = mod[funcName];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load handler:', err.message);
|
console.error('Failed to load handler:', err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Изменено: 2026-03-07
|
# Изменено: 2026-03-08
|
||||||
# HTTP-обёртка для serverless функций на Python 3.11.
|
# HTTP-обёртка для serverless функций на Python 3.11.
|
||||||
# Загружает handler.py из /app/function/ и вызывает handle(event) на каждый запрос.
|
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
|
||||||
|
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
|
||||||
# Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path.
|
# Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
HANDLER_PATH = "/app/function/handler.py"
|
|
||||||
PORT = 8080
|
PORT = 8080
|
||||||
|
|
||||||
|
# Разбираем SLESS_ENTRYPOINT="module.func" → файл и имя функции.
|
||||||
|
# Fallback: handler.py + handle — для обратной совместимости.
|
||||||
|
_entrypoint = os.environ.get("SLESS_ENTRYPOINT", "handler.handle")
|
||||||
|
_dot_idx = _entrypoint.rfind(".")
|
||||||
|
_module_name = _entrypoint[:_dot_idx] if _dot_idx >= 0 else _entrypoint
|
||||||
|
_func_name = _entrypoint[_dot_idx + 1:] if _dot_idx >= 0 else "handle"
|
||||||
|
HANDLER_PATH = f"/app/function/{_module_name}.py"
|
||||||
|
|
||||||
|
|
||||||
def load_handler():
|
def load_handler():
|
||||||
# Загружаем модуль пользователя динамически — путь известен только в runtime.
|
# Загружаем модуль пользователя динамически — путь известен только в runtime.
|
||||||
spec = importlib.util.spec_from_file_location("handler", HANDLER_PATH)
|
spec = importlib.util.spec_from_file_location(_module_name, HANDLER_PATH)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
if not hasattr(module, "handle"):
|
if not hasattr(module, _func_name):
|
||||||
raise AttributeError(f"handler.py must define a 'handle(event)' function")
|
raise AttributeError(f"{HANDLER_PATH} must define a '{_func_name}(event)' function")
|
||||||
return module.handle
|
return getattr(module, _func_name)
|
||||||
|
|
||||||
|
|
||||||
_handle = load_handler()
|
_handle = load_handler()
|
||||||
|
|||||||
Reference in New Issue
Block a user