diff --git a/controllers/function_controller.go b/controllers/function_controller.go index aa8160a..d139607 100644 --- a/controllers/function_controller.go +++ b/controllers/function_controller.go @@ -7,9 +7,12 @@ package controllers import ( + "bytes" "context" "fmt" + "io" "sort" + "strings" "time" appsv1 "k8s.io/api/apps/v1" @@ -19,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -33,9 +37,10 @@ type FunctionReconciler struct { client.Client Scheme *runtime.Scheme Builder *builder.Builder - RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций) - OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-* - HarborClient *harbor.Client // nil — Harbor не используется, EnsureProject пропускается + KubeClient kubernetes.Interface // typed client для чтения логов build-подов + RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций) + OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-* + HarborClient *harbor.Client // nil — Harbor не используется, EnsureProject пропускается } //+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete @@ -167,7 +172,13 @@ func (r *FunctionReconciler) checkBuild(ctx context.Context, fn *slessv1alpha1.F return ctrl.Result{Requeue: true}, nil case "failed": - return r.setFailed(ctx, fn, "build job failed") + // Захватываем логи build-пода чтобы разработчик видел причину ошибки (pip error и т.д.). + logs := getBuildPodLogs(ctx, r.KubeClient, r.OperatorNamespace, jobName) + msg := "build job failed" + if logs != "" { + msg = "build job failed:\n" + logs + } + return r.setFailed(ctx, fn, msg) } return ctrl.Result{RequeueAfter: 10 * time.Second}, nil @@ -389,3 +400,37 @@ func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&slessv1alpha1.Function{}). Complete(r) } + +// getBuildPodLogs возвращает логи (stderr+stdout) пода kaniko build Job'а. +// Используется чтобы пробросить ошибку pip/kaniko в Function.Status.Message. +// Возвращает не более 50 последних строк — достаточно для диагностики, не засоряет CRD. +// Если логи недоступны — возвращает пустую строку (caller покажет generic msg). +func getBuildPodLogs(ctx context.Context, kube kubernetes.Interface, namespace, jobName string) string { + if kube == nil { + return "" + } + pods, err := kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: "job-name=" + jobName, + }) + if err != nil || len(pods.Items) == 0 { + return "" + } + req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{}) + stream, err := req.Stream(ctx) + if err != nil { + return "" + } + defer stream.Close() + buf := new(bytes.Buffer) + _, _ = io.Copy(buf, stream) + raw := strings.TrimSpace(buf.String()) + if raw == "" { + return "" + } + // Оставляем последние 50 строк — ошибки pip всегда в конце вывода. + lines := strings.Split(raw, "\n") + if len(lines) > 50 { + lines = lines[len(lines)-50:] + } + return strings.Join(lines, "\n") +} diff --git a/deployments/k8s/operator.yaml b/deployments/k8s/operator.yaml index c059479..1ef320c 100644 --- a/deployments/k8s/operator.yaml +++ b/deployments/k8s/operator.yaml @@ -72,7 +72,7 @@ spec: containers: - name: operator # При обновлении версии оператора — менять тег здесь (не latest!) - image: naeel/sless-operator:v0.1.26 + image: naeel/sless-operator:v0.1.27 # Always — чтобы всегда тянуть по точному тегу (не кешировать старый) imagePullPolicy: Always ports: diff --git a/main.go b/main.go index bd20ffd..37564eb 100644 --- a/main.go +++ b/main.go @@ -144,6 +144,7 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Builder: bldr, + KubeClient: kubernetes.NewForConfigOrDie(mgr.GetConfig()), RegistrySecret: cfg.RegistrySecret, OperatorNamespace: "sless", HarborClient: harborClient, diff --git a/runtimes/python3.11/server.py b/runtimes/python3.11/server.py index b2cdd42..3bd9969 100644 --- a/runtimes/python3.11/server.py +++ b/runtimes/python3.11/server.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Изменено: 2026-03-09 +# Изменено: 2026-03-11 # HTTP-обёртка для serverless функций на Python 3.11. # Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию. # Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle) @@ -65,17 +65,49 @@ class FunctionHandler(BaseHTTPRequestHandler): event = self._parse_request_meta({}) self._respond(200, _handle(event)) - def do_POST(self): + def _handle_with_body(self): + # Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.) + # Читаем тело только если Content-Length > 0, иначе передаём пустой event. length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(length) + body = self.rfile.read(length) if length > 0 else b"" try: event = json.loads(body) if body else {} except json.JSONDecodeError: # Если тело не JSON — передаём как строку, не ломаем вызов event = {"body": body.decode("utf-8", errors="replace")} event = self._parse_request_meta(event) - result = _handle(event) - self._respond(200, result) + self._respond(200, _handle(event)) + + def do_POST(self): + self._handle_with_body() + + # PUT, DELETE, PATCH — передаём в функцию как обычные вызовы. + # event['_method'] позволяет функции роутить по методу внутри. + do_PUT = _handle_with_body + do_DELETE = _handle_with_body + do_PATCH = _handle_with_body + + def do_HEAD(self): + # HEAD: те же заголовки что и GET, но без тела (HTTP-стандарт). + if self.path == "/health": + result = {"status": "ok"} + else: + result = _handle(self._parse_request_meta({})) + body = json.dumps(result).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + # Тело не отправляем — семантика HEAD + + def send_error(self, code, message=None, explain=None): + # Переопределяем дефолтный HTML-ответ на JSON — пользователь всегда получает JSON. + body = json.dumps({"error": message or f"HTTP {code}", "code": code}).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) def _respond(self, status, data): body = json.dumps(data).encode("utf-8")