Compare commits

...
12 Commits
Author SHA1 Message Date
Naeel 6d66e5b566 feat: multi-language support — PHP, Ruby, Perl, Go (binary-env)
- PHP: ghcr.io/fission/php-env, entrypoint main.php::handler
- Ruby: ghcr.io/fission/ruby-env, entrypoint handler (single-word)
- Perl: ghcr.io/fission/perl-env (version=1), sub {} return value
- Go: ghcr.io/fission/binary-env with pre-compiled binary (shell script placeholder)

Provider changes:
- package_resource: added main.php, main.rb, main.pl to source_dir candidates
- function_resource: relaxed entrypoint validation to allow PHP (::), Ruby, Perl (single-word)
- function_resource_test: updated BadFormat test to reflect new valid formats
2026-04-15 17:01:40 +03:00
Naeel c78b539d24 doc: plan for multi-language support (Go, Java, PHP, Ruby, .NET, Perl) 2026-04-15 16:31:34 +03:00
Naeel cccc5ca024 revert: remove Go function (runtime compilation timeout) 2026-04-15 16:24:57 +03:00
Naeel 66404fde3a add: Go function example (tf-go-hello-fn) 2026-04-15 16:19:25 +03:00
Naeel 4964c8f357 console: fix favicon — full base64 data URI (v0.3.4) 2026-04-15 15:20:27 +03:00
Naeel 7e23a71574 console: embed favicon as data URI (v0.3.3) 2026-04-15 15:15:21 +03:00
Naeel ab2eff69a0 ui: use terra.k8c.ru favicon (same as qu.kube5s.ru/ui/) 2026-04-15 15:08:58 +03:00
Naeel a199745c77 fix: restore 15 functions and roll console favicon image 2026-04-15 15:05:27 +03:00
Naeel ba32389600 ui: use nubes favicon url 2026-04-15 14:51:10 +03:00
Naeel 8ac8428a76 docs: record strict cleanup to 6 verified functions 2026-04-15 14:43:58 +03:00
Naeel ba50d18f8f fix(console): sync function package resourceversion after code update 2026-04-15 11:14:42 +03:00
Naeel d9a6d2f29e fix(console): decode zip package literal before showing code in UI 2026-04-15 11:00:00 +03:00
23 changed files with 1579 additions and 32 deletions
+3
View File
@@ -8,6 +8,9 @@ bin/
*.test *.test
*.out *.out
# Compiled function binaries (generated, not versioned)
examples/*/dist/
# Provider binaries # Provider binaries
terraform-provider-fission terraform-provider-fission
terraform-provider-fission_* terraform-provider-fission_*
+5 -1
View File
@@ -46,7 +46,7 @@ spec:
serviceAccountName: fission-console serviceAccountName: fission-console
containers: containers:
- name: console - name: console
image: naeel/fission-console:v0.2.3 image: naeel/fission-console:v0.3.4
ports: ports:
- containerPort: 8090 - containerPort: 8090
env: env:
@@ -56,6 +56,10 @@ spec:
value: "http://router.fission.svc.cluster.local" value: "http://router.fission.svc.cluster.local"
- name: PORT - name: PORT
value: "8090" value: "8090"
- name: FISSION_HTTP_TIMEOUT
value: "30s"
- name: FISSION_INVOKE_TIMEOUT
value: "20s"
- name: FISSION_AUTH_USERNAME - name: FISSION_AUTH_USERNAME
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
+219 -22
View File
@@ -1,18 +1,23 @@
package main package main
import ( import (
"archive/zip"
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"net"
"net/http" "net/http"
"os" "os"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode/utf8"
"fission-console/ui" "fission-console/ui"
@@ -36,11 +41,12 @@ var (
const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
type server struct { type server struct {
dyn dynamic.Interface dyn dynamic.Interface
ns string ns string
routerURL string routerURL string
http *http.Client http *http.Client
saTokenPath string saTokenPath string
invokeTimeout time.Duration
authUser string authUser string
authPass string authPass string
@@ -68,6 +74,8 @@ func main() {
namespace := envDefault("FISSION_NAMESPACE", "default") namespace := envDefault("FISSION_NAMESPACE", "default")
routerURL := strings.TrimRight(envDefault("FISSION_ROUTER_URL", "http://router.fission.svc.cluster.local"), "/") routerURL := strings.TrimRight(envDefault("FISSION_ROUTER_URL", "http://router.fission.svc.cluster.local"), "/")
port := envDefault("PORT", "8090") port := envDefault("PORT", "8090")
httpTimeout := envDurationDefault("FISSION_HTTP_TIMEOUT", 30*time.Second)
invokeTimeout := envDurationDefault("FISSION_INVOKE_TIMEOUT", 20*time.Second)
cfg, err := buildConfig(kubeconfig) cfg, err := buildConfig(kubeconfig)
if err != nil { if err != nil {
@@ -84,13 +92,14 @@ func main() {
saTokenPath := envDefault("SA_TOKEN_PATH", defaultSATokenPath) saTokenPath := envDefault("SA_TOKEN_PATH", defaultSATokenPath)
s := &server{ s := &server{
dyn: dyn, dyn: dyn,
ns: namespace, ns: namespace,
routerURL: routerURL, routerURL: routerURL,
http: &http.Client{Timeout: 30 * time.Second}, http: &http.Client{Timeout: httpTimeout},
saTokenPath: saTokenPath, saTokenPath: saTokenPath,
authUser: authUser, invokeTimeout: invokeTimeout,
authPass: authPass, authUser: authUser,
authPass: authPass,
} }
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -123,7 +132,7 @@ func main() {
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":" + port, Addr: ":" + port,
Handler: withCORS(logRequests(mux)), Handler: withSecurityHeaders(withCORS(logRequests(mux))),
ReadHeaderTimeout: 10 * time.Second, ReadHeaderTimeout: 10 * time.Second,
} }
@@ -338,13 +347,7 @@ func (s *server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
if packageName != "" { if packageName != "" {
pkg, pkgErr := s.dyn.Resource(packageGVR).Namespace(s.ns).Get(ctx, packageName, metav1.GetOptions{}) pkg, pkgErr := s.dyn.Resource(packageGVR).Namespace(s.ns).Get(ctx, packageName, metav1.GetOptions{})
if pkgErr == nil { if pkgErr == nil {
literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal") code = s.extractPackageSourceCode(ctx, pkg)
if literal != "" {
decoded, decErr := base64.StdEncoding.DecodeString(literal)
if decErr == nil {
code = string(decoded)
}
}
} }
} }
@@ -428,7 +431,23 @@ func (s *server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
return return
} }
writeAnyJSON(w, http.StatusOK, map[string]any{"updated": true, "package": pkgName}) updatedPkg, err := s.dyn.Resource(packageGVR).Namespace(s.ns).Get(ctx, pkgName, metav1.GetOptions{})
if err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get updated package %q: %v", pkgName, err))
return
}
if err := unstructured.SetNestedField(fn.Object, updatedPkg.GetResourceVersion(), "spec", "package", "packageref", "resourceversion"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function package resourceversion: %v", err))
return
}
if _, err := s.dyn.Resource(functionGVR).Namespace(s.ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update function %q package ref: %v", name, err))
return
}
writeAnyJSON(w, http.StatusOK, map[string]any{"updated": true, "package": pkgName, "package_resourceversion": updatedPkg.GetResourceVersion()})
} }
func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, name string) { func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, name string) {
@@ -441,7 +460,12 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
bodyBytes = []byte("{}") bodyBytes = []byte("{}")
} }
ctx, cancel := context.WithTimeout(r.Context(), 25*time.Second) invokeTimeout := s.invokeTimeout
if invokeTimeout <= 0 {
invokeTimeout = 20 * time.Second
}
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
defer cancel() defer cancel()
invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name) invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name)
@@ -498,6 +522,15 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
resp, err := s.http.Do(req) resp, err := s.http.Do(req)
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s: function specialization likely failed (for example, syntax error)", name, invokeTimeout))
return
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s: function specialization likely failed (for example, syntax error)", name, invokeTimeout))
return
}
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", name, err)) writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", name, err))
return return
} }
@@ -676,6 +709,17 @@ func withCORS(next http.Handler) http.Handler {
}) })
} }
func withSecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content")
next.ServeHTTP(w, r)
})
}
func envDefault(key, fallback string) string { func envDefault(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" { if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v return v
@@ -683,6 +727,19 @@ func envDefault(key, fallback string) string {
return fallback return fallback
} }
func envDurationDefault(key string, fallback time.Duration) time.Duration {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
d, err := time.ParseDuration(raw)
if err != nil || d <= 0 {
log.Printf("invalid duration for %s=%q, using default %s", key, raw, fallback)
return fallback
}
return d
}
func normalizeMethods(in []string) []string { func normalizeMethods(in []string) []string {
if len(in) == 0 { if len(in) == 0 {
return []string{"GET"} return []string{"GET"}
@@ -702,3 +759,143 @@ func normalizeMethods(in []string) []string {
} }
return out return out
} }
func (s *server) extractPackageSourceCode(ctx context.Context, pkg *unstructured.Unstructured) string {
literalPaths := [][]string{
{"spec", "source", "literal"},
{"spec", "deployment", "literal"},
}
for _, p := range literalPaths {
literal, found, _ := unstructured.NestedString(pkg.Object, p...)
if !found || strings.TrimSpace(literal) == "" {
continue
}
if decodedCode, decErr := decodeLiteralToSource(literal); decErr == nil && strings.TrimSpace(decodedCode) != "" {
return decodedCode
}
}
urlPaths := [][]string{
{"spec", "source", "url"},
{"spec", "deployment", "url"},
}
for _, p := range urlPaths {
urlValue, found, _ := unstructured.NestedString(pkg.Object, p...)
if !found || strings.TrimSpace(urlValue) == "" {
continue
}
archiveBytes, fetchErr := s.fetchPackageArchive(ctx, urlValue)
if fetchErr != nil {
continue
}
decodedCode, decErr := decodeArchiveBytesToSource(archiveBytes)
if decErr == nil && strings.TrimSpace(decodedCode) != "" {
return decodedCode
}
}
return ""
}
func (s *server) fetchPackageArchive(ctx context.Context, archiveURL string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, archiveURL, nil)
if err != nil {
return nil, err
}
resp, err := s.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("archive request failed: %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
func decodeLiteralToSource(literal string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(literal)
if err != nil {
return "", err
}
return decodeArchiveBytesToSource(decoded)
}
func decodeArchiveBytesToSource(decoded []byte) (string, error) {
if len(decoded) == 0 {
return "", fmt.Errorf("empty payload")
}
if utf8.Valid(decoded) {
return string(decoded), nil
}
if len(decoded) >= 4 && bytes.Equal(decoded[:4], []byte{'P', 'K', 3, 4}) {
if src, zipErr := decodeZipSource(decoded); zipErr == nil {
return src, nil
}
}
return "", fmt.Errorf("payload does not contain utf-8 source")
}
func decodeZipSource(zipBytes []byte) (string, error) {
reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
return "", err
}
preferred := []string{"main.py", "main.js", "main.go"}
for _, name := range preferred {
for _, file := range reader.File {
if strings.EqualFold(file.Name, name) {
content, readErr := readZipFile(file)
if readErr != nil {
return "", readErr
}
if utf8.Valid(content) {
return string(content), nil
}
}
}
}
files := make([]*zip.File, 0, len(reader.File))
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
files = append(files, file)
}
sort.Slice(files, func(i, j int) bool {
return files[i].Name < files[j].Name
})
for _, file := range files {
content, readErr := readZipFile(file)
if readErr != nil {
continue
}
if utf8.Valid(content) {
return string(content), nil
}
}
return "", fmt.Errorf("zip archive does not contain utf-8 source files")
}
func readZipFile(file *zip.File) ([]byte, error) {
rc, err := file.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
+71
View File
@@ -142,6 +142,77 @@ func TestUpdateFunctionCode(t *testing.T) {
} }
} }
func TestGetFunctionUsesSourceLiteralWhenDeploymentLiteralMissing(t *testing.T) {
env := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Environment",
"metadata": map[string]any{"name": "go-acc", "namespace": "default"},
}}
s := newTestServer(env)
pkg := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Package",
"metadata": map[string]any{
"name": "fn-go-acc-pkg",
"namespace": "default",
},
"spec": map[string]any{
"source": map[string]any{
"literal": base64.StdEncoding.EncodeToString([]byte("package main\n\nfunc Handler() {}\n")),
},
"deployment": map[string]any{
"type": "url",
"url": "http://storagesvc.fission/v1/archive?id=dummy",
},
},
}}
fn := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Function",
"metadata": map[string]any{
"name": "fn-go-acc",
"namespace": "default",
},
"spec": map[string]any{
"environment": map[string]any{"name": "go-acc", "namespace": "default"},
"package": map[string]any{
"functionName": "Handler",
"packageref": map[string]any{
"name": "fn-go-acc-pkg",
"namespace": "default",
},
},
},
}}
if _, err := s.dyn.Resource(packageGVR).Namespace("default").Create(context.Background(), pkg, metav1.CreateOptions{}); err != nil {
t.Fatalf("create package: %v", err)
}
if _, err := s.dyn.Resource(functionGVR).Namespace("default").Create(context.Background(), fn, metav1.CreateOptions{}); err != nil {
t.Fatalf("create function: %v", err)
}
getReq := httptest.NewRequest(http.MethodGet, "/api/functions/fn-go-acc", nil)
getRec := httptest.NewRecorder()
s.handleGetFunction(getRec, getReq, "fn-go-acc")
if getRec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", getRec.Code, getRec.Body.String())
}
var out map[string]any
if err := json.Unmarshal(getRec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode get response: %v", err)
}
code, _ := out["code"].(string)
if !strings.Contains(code, "func Handler") {
t.Fatalf("expected source code from spec.source.literal, got %q", code)
}
}
func TestInvokeFunctionWithJWTAuth(t *testing.T) { func TestInvokeFunctionWithJWTAuth(t *testing.T) {
// Mock router: /auth/login returns JWT, /inv-fn returns hello // Mock router: /auth/login returns JWT, /inv-fn returns hello
var gotAuth string var gotAuth string
+53 -2
View File
@@ -3,7 +3,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fission Console</title> <title>NUBES Fission Console</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA4ZJREFUeJztm8+Lm0UYxz/fySZUoWIrVtbmXfyBP05Fa3e3rogsglJRK0p7E2svnrz6B3gQ8eRJFooXET14KKJ4FUQo3UQQ9CRC6W4WQW0VXZduTd7HQ9c22bzbTCY/ZpO8n0syb56Z+c437zPvhMzIzJhkXGwBsckNiC0gNrkBsQXEZsonSJLj4GNPgb2ACo9j9gDOvrSV6qkB6+sJ6WSJ5MIxUo4h5sDuI9U7tlZ57/+YTAN0zyOPUi+9jOxJxMOUj9wJOBBg117QvqGMwhNpcQ/J+rOkPIc0j+xeytyGsaWXa2+kvc312gxQMleHYgFtrQ9GYJmgmSfup3z15xuDNW/dGXOAjd680NgshlYdvcH2mfEwQIU0tOp4GNADuQGxBcRmPAyQgh/W42FAD+QGxBYQm/EwIF8HhJMbEFtAX3DreQqEkhsQW0BsxsMAlz8Gg8kNiC0gNuNhgJvK54BQcgNiC+gLhUlPgcuTbkAP5AbEFhCbcAOM4LyLitIW3eEGyP7qWUwMHFdaixNO1v4Av5pG8H/yfWfvesk7tq5GczHDAHnmtu727nTQbJYe8o51+qOl2BYgNv1a0iHvTgeNGq94x6buYnOx3YCU3/1asn06ePQZ744HiXTCO3a6/m1zsd0Ax/f+HTc+kU76598AUDL/LpjfjjWxYdXqRvOljDvAfezfO3dQvvCDtLjHu04f0czsaSx9y7uC8eP2S20GWO38Z+A7DwDwIOX135TMvylpKI9VJQv7lcx+hfEhurELsCPOfdDWVtZ5ASWzZ4GXArQ1QJfA/kTW6BhtLgX+7qL9IqTTmKa7GjgAumKry7dsv5q9Vfbfq29QLL1I9wulAtgB4ADmoy9kY4fodugAODuT2dpOJ0aUzH8E6asBXe0+zP5h7bvbzay+/aOdv+Ha8mnE5YEKGxqF17MGDzcxwMzqTBUWgc65vKuxT7cm9kx2TIHrATOzxzHOEpZ5sanYamXuZgEdJzlbqXyOmzrO6N0J56hVj3YK8prl7eK5L6g3DmFc6l3XEDCWbLWyYGYdf9h1TIGWYMmRHDlDaqcY0qKnO/Qr4oStLH/jXSPk4KSShf2ovoTZ80Db4iICv2D2ttWqS91WDDKgpYHy4adR8TWww8BdmN2KrOg5Zwoo+PV0/ThICmyArWHua9D7Vjv/U5j6Phgw6uzCPB4uuQGxBcRm4g34D1IN9ODV8oQpAAAAAElFTkSuQmCC">
<script>
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
window.location.replace('https://' + window.location.host + window.location.pathname + window.location.search + window.location.hash);
}
</script>
<style> <style>
:root { :root {
--bg-page: #001120; --bg-page: #001120;
@@ -39,6 +45,45 @@
letter-spacing: .3px; letter-spacing: .3px;
} }
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-mark {
width: 30px;
height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, #009dff 0%, #0055d4 100%);
color: #fff;
display: grid;
place-items: center;
font-size: 14px;
font-weight: 800;
letter-spacing: .02em;
box-shadow: 0 6px 18px rgba(0, 125, 255, .35);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.08;
}
.brand-text .nubes {
font-size: 12px;
letter-spacing: .22em;
font-weight: 800;
color: #8bc7ff;
}
.brand-text .product {
font-size: 13px;
font-weight: 700;
letter-spacing: .04em;
}
.btn { .btn {
background: var(--accent); background: var(--accent);
color: #fff; color: #fff;
@@ -243,7 +288,13 @@
</head> </head>
<body> <body>
<div class="navbar"> <div class="navbar">
<div class="title">sless / Fission Console</div> <div class="brand">
<div class="brand-mark">N</div>
<div class="brand-text">
<div class="nubes">NUBES</div>
<div class="product">FISSION CONSOLE</div>
</div>
</div>
<div class="row" style="margin:0;"> <div class="row" style="margin:0;">
<button class="btn ghost" onclick="reloadAll()">Refresh</button> <button class="btn ghost" onclick="reloadAll()">Refresh</button>
<button class="btn" onclick="openCreate()">+ Create Function</button> <button class="btn" onclick="openCreate()">+ Create Function</button>
+399
View File
@@ -0,0 +1,399 @@
# План: поддержка всех языков Fission
**Дата:** 2026-04-15
**Исполнитель:** Sonnet / GPT 5.3 Codex (AI-агент)
**Цель:** Добавить рабочие функции на Go, Java, .NET, Ruby, Rust, PHP — и ПРОТЕСТИРОВАТЬ каждую
---
## Текущее состояние
| Язык | Статус | Проблемы |
|---|---|---|
| Python | ✅ Работает | — |
| Node.js | ✅ Работает | — |
| Go | ❌ Timeout | Runtime compilation > 20s specialization timeout |
| Java | ❌ Не развёрнуто | — |
| .NET | ❌ Не развёрнуто | — |
| Ruby | ❌ Не развёрнуто | — |
| Rust | ❌ Не развёрнуто | — |
| PHP | ❌ Не развёрнуто | — |
---
## КРИТИЧЕСКИЕ ПРАВИЛА (обязательно для агента)
1. **ВСЕ КОМАНДЫ — ТОЛЬКО ЧЕРЕЗ SSH:**
```bash
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
```
2. **Файлы редактировать можно локально**`/home/naeel/remote_dev/fission/` = `~/terra/fission/` на VM
3. **Terraform деплой:**
```bash
ssh ... 'cd ~/terra/fission/examples/FOLDER && TF_CLI_CONFIG_FILE=/tmp/terraformrc-fission terraform apply -auto-approve'
```
4. **Проверка функции (JWT обязателен):**
```bash
ssh ... '
PASSWORD=$(kubectl -n fission get secret router -o jsonpath={.data.password} | base64 -d)
TOKEN=$(curl -sk -X POST https://fission.kube5s.ru/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"$PASSWORD\"}" | python3 -c "import sys,json; print(json.load(sys.stdin)[\"accesstoken\"])")
curl -sk -w "\nHTTP=%{http_code}\n" -H "Authorization: Bearer $TOKEN" https://fission.kube5s.ru/ENDPOINT
'
```
5. **НЕ ТРОГАТЬ существующие функции** — 15 шт., список в copilot-instructions.md
---
## Шаг 0: Проверить доступность образов (ОБЯЗАТЕЛЬНО ПЕРВЫМ)
Перед созданием — проверить что образы exist на ghcr.io:
```bash
ssh ... '
for lang in go java jvm dotnet dotnet20 ruby perl php binary; do
echo "--- $lang ---"
# Попробовать pull (dry-run)
docker pull ghcr.io/fission/${lang}-env:latest 2>&1 | head -3
echo
done
'
```
Если образ не найден — попробовать варианты:
- `ghcr.io/fission/go-env`
- `ghcr.io/fission/jvm-env` (Java = JVM в Fission!)
- `ghcr.io/fission/dotnet20-env` или `ghcr.io/fission/dotnet-env`
- `ghcr.io/fission/ruby-env`
- `ghcr.io/fission/php-env`
- `ghcr.io/fission/binary-env` (для предкомпилированных)
- `ghcr.io/fission/perl-env`
Если образ не существует — НЕ создавать функцию, пропустить язык.
---
## Шаг 1: Go (ПРОБЛЕМНЫЙ — требует особого подхода)
### Проблема
Go env компилирует код при specialization → timeout 20s → функция не работает.
### Решение: использовать `binary-env`
Go компилируется на VM → заливается как бинарник → binary-env его запускает.
**Или:** увеличить specialization timeout через InvokeStrategy:
```yaml
spec:
InvokeStrategy:
ExecutionStrategy:
ExecutorType: newdeploy # вместо poolmgr
MinScale: 1 # always running
MaxScale: 3
```
### Вариант A: pre-compiled + binary-env
```
examples/go-hello/
├── code/
│ └── main.go # исходник для справки
├── build.sh # скрипт для компиляции
└── main.tf
```
**build.sh:**
```bash
#!/bin/bash
cd code
CGO_ENABLED=0 GOOS=linux go build -o handler main.go
```
**main.tf** — использовать `binary-env` вместо `go-env`
### Вариант B: newdeploy executor
В Terraform manifests задать `executor_type = "newdeploy"` и `min_scale = 1`.
Проверить поддерживает ли наш провайдер эти аргументы:
```bash
ssh ... 'grep -r "executor\|newdeploy\|min_scale\|invoke_strategy" ~/terra/fission/internal/'
```
### Код функции (main.go):
```go
package main
import (
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("Hello from Go in Fission"))
}
```
### Тест: HTTP 200, body содержит "Hello from Go"
---
## Шаг 2: Java (JVM)
### Образ: `ghcr.io/fission/jvm-env`
### Структура:
```
examples/java-hello/
├── code/
│ └── io/fission/Function.java
└── main.tf
```
### Код:
```java
package io.fission;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
public class Function implements io.fission.Function {
@Override
public ResponseEntity<?> call(RequestEntity req, io.fission.Context context) {
return ResponseEntity.ok("Hello from Java in Fission");
}
}
```
**ВАЖНО:** Проверить точный интерфейс JVM env. Может потребоваться другой формат:
```bash
ssh ... 'docker run --rm ghcr.io/fission/jvm-env:latest cat /app/README.md 2>/dev/null || echo "No README"'
```
### main.tf:
```hcl
resource "fission_environment" "jvm" {
name = "tf-jvm-hello-env"
image = "ghcr.io/fission/jvm-env"
version = 3
}
# ... стандартный pattern
```
### Тест: HTTP 200, body содержит "Hello from Java"
---
## Шаг 3: PHP
### Образ: `ghcr.io/fission/php-env`
### Структура:
```
examples/php-hello/
├── code/
│ └── hello.php
└── main.tf
```
### Код:
```php
<?php
return function() {
return "Hello from PHP in Fission";
};
```
**ВАЖНО:** entrypoint для PHP — имя файла без расширения (`hello`).
### Тест: HTTP 200, body содержит "Hello from PHP"
---
## Шаг 4: Ruby
### Образ: `ghcr.io/fission/ruby-env`
### Структура:
```
examples/ruby-hello/
├── code/
│ └── hello.rb
└── main.tf
```
### Код:
```ruby
def main
"Hello from Ruby in Fission"
end
```
### Тест: HTTP 200, body содержит "Hello from Ruby"
---
## Шаг 5: .NET (C#)
### Образ: `ghcr.io/fission/dotnet-env` или `ghcr.io/fission/dotnet20-env`
### Структура:
```
examples/dotnet-hello/
├── code/
│ └── FissionFunction.cs
└── main.tf
```
### Код:
```csharp
using System;
using Fission.DotNetCore.Api;
public class FissionFunction
{
public string Execute(FissionContext context)
{
return "Hello from .NET in Fission";
}
}
```
### Тест: HTTP 200, body содержит "Hello from .NET"
---
## Шаг 6: Rust
### ВЕРОЯТНО НЕ СУЩЕСТВУЕТ официально.
Проверить:
```bash
ssh ... 'docker pull ghcr.io/fission/rust-env:latest 2>&1'
```
Если нет — пропустить. Rust можно реализовать через `binary-env` (pre-compiled).
---
## Шаг 7: Perl (бонус)
### Образ: `ghcr.io/fission/perl-env`
```perl
sub main {
return "Hello from Perl in Fission";
}
```
---
## Порядок выполнения (ВАЖНО)
```
1. Проверить доступность ВСЕХ образов (docker pull) → составить список реальных
2. Для каждого доступного языка:
a. Создать examples/LANG-hello/code/... + main.tf
b. terraform apply
c. curl с JWT → проверить HTTP 200 + ожидаемый body
d. Если не работает — смотреть логи:
kubectl -n default get events --sort-by=".lastTimestamp" | tail -20
kubectl -n default get pods | grep poolmgr-LANG
kubectl -n default logs <pod> -c <container> --tail=30
e. Если timeout — попробовать newdeploy executor
f. Если всё равно не работает — удалить через terraform destroy
3. Обновить список в консоли (она автоматически видит новые функции)
4. Протестировать все через консоль (invoke)
5. Коммит + пуш + тег
```
---
## Критерий успеха
| Язык | Endpoint | Ожидаемый body | HTTP |
|---|---|---|---|
| Go | `/go-hello` | `Hello from Go in Fission` | 200 |
| Java | `/java-hello` | `Hello from Java in Fission` | 200 |
| PHP | `/php-hello` | `Hello from PHP in Fission` | 200 |
| Ruby | `/ruby-hello` | `Hello from Ruby in Fission` | 200 |
| .NET | `/dotnet-hello` | `Hello from .NET in Fission` | 200 |
| Perl | `/perl-hello` | `Hello from Perl in Fission` | 200 |
**Минимум:** 3 новых языка работают (Go + ещё 2)
**Идеал:** все 6
---
## Шаблон main.tf (копировать и менять)
```hcl
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "env" {
name = "tf-LANG-hello-env"
image = "ghcr.io/fission/LANG-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-LANG-hello-pkg"
environment = fission_environment.env.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-LANG-hello-fn"
environment = fission_environment.env.name
package_name = fission_package.pkg.name
entrypoint = "ENTRYPOINT"
}
resource "fission_http_trigger" "route" {
name = "tf-LANG-hello-route"
function = fission_function.fn.name
url = "/LANG-hello"
methods = ["GET"]
}
```
---
## Entrypoint для каждого языка
| Язык | Entrypoint | Файл |
|---|---|---|
| Python | `main.main` | `main.py` |
| Node.js | `module.exports` (пустой) | `server.js` |
| Go | `Handler` | `main.go` |
| Java | `io.fission.Function` | `Function.java` |
| PHP | `hello` | `hello.php` |
| Ruby | `hello.main` | `hello.rb` |
| .NET | `FissionFunction.Execute` | `FissionFunction.cs` |
| Perl | `hello.main` | `hello.pm` |
**⚠️ ВНИМАНИЕ:** Entrypoints могут отличаться от указанных! Проверять документацию каждого env:
```bash
ssh ... 'docker run --rm ghcr.io/fission/LANG-env:latest env 2>/dev/null | head -20'
```
---
## Откат при неудаче
Если язык не работает:
```bash
ssh ... 'cd ~/terra/fission/examples/LANG-hello && TF_CLI_CONFIG_FILE=/tmp/terraformrc-fission terraform destroy -auto-approve'
rm -rf examples/LANG-hello/
```
+221
View File
@@ -258,6 +258,30 @@
#### Правила репозитория #### Правила репозитория
Создан `.github/copilot-instructions.md` с правилами: Создан `.github/copilot-instructions.md` с правилами:
### Обновление этапа (UI invoke stability: JS)
- Проведена диагностика JS invoke через реальный UI-путь (`/console/api/functions/{name}/invoke`) и роутер/экзекьютор логи.
- Найдена корневая причина JS-таймаутов:
- для `node-env` используется `v2/specialize`
- при `functionName=main` runtime пытался загрузить `/userfunc/deployarchive/main`
- при plain literal `deployarchive` является файлом, не директорией
- после specialize без корректного контракта запросы зависали и упирались в router roundtripper timeout.
- Для `nodejs-acc` зафиксирован runtime image: `ghcr.io/fission/node-env:1.32.5`.
- Для JS-функций выровнен контракт runtime:
- `spec.package.functionName` выставлен в пустой entrypoint (`""`) для default export
- код приведен к формату ответа Node runtime: `return { status: 200, body: "..." }`
- для основных JS-маршрутов включены методы `GET` + `POST`.
- Подтверждена работоспособность через console invoke:
- `fn-js-acc` -> `status=200`, `response_raw=hello-js-ok`
- `fn-js-direct` -> `status=200`, `response_raw=hello-js-ok`
- тестовая матрица `jsm1..jsm4` -> `status=200`.
### Статус после фикса
- Python invoke: работает.
- JS invoke: работает по UI-пути и по прямому GET роутов.
- Go invoke (`fn-go-acc`): остается отдельной runtime-проблемой (вне JS-фикса).
- Маппинг путей (локально ~/remote_dev/ = ВМ ~/terra/) - Маппинг путей (локально ~/remote_dev/ = ВМ ~/terra/)
- Редактирование файлов — разрешено локально - Редактирование файлов — разрешено локально
- Команды — ИСКЛЮЧИТЕЛЬНО через SSH (VPN-конфликты) - Команды — ИСКЛЮЧИТЕЛЬНО через SSH (VPN-конфликты)
@@ -285,3 +309,200 @@
### Ограничения ### Ограничения
- Runtime-проблемы Fission (зависания на cold start/таймауты выполнения) в этом изменении не трогались: исправлен только provider-слой валидации и обнаружения изменений. - Runtime-проблемы Fission (зависания на cold start/таймауты выполнения) в этом изменении не трогались: исправлен только provider-слой валидации и обнаружения изменений.
## 2026-04-15 (дополнение) — Fix отображения кода в Console UI
### Проблема
- В модальном окне редактирования функции (`Edit Code`) для некоторых пакетов отображались байты ZIP (`PK...`) вместо исходного кода.
### Причина
- `GET /console/api/functions/{name}` декодировал `spec.deployment.literal` только из base64, но не обрабатывал архивированный payload.
### Исправление
- В `console/main.go` добавлено декодирование `literal` с поддержкой ZIP:
- если payload plain text/utf-8 -> отдаётся как есть
- если payload ZIP -> извлекается `main.py`/`main.js`/`main.go` (или первый utf-8 файл)
- Обновлён deployment image: `naeel/fission-console:v0.2.4`.
### Проверка
- `GET /console/api/functions/fn-go-acc` теперь возвращает читаемый Go-код, без `PK...` сигнатур.
## 2026-04-15 (дополнение) — Полный пользовательский прогон UI и invoke-fix
### Что проверено как пользовательский сценарий
- Прогнан массовый invoke через UI API (`POST /console/api/functions/{name}/invoke`) для всех функций в списке.
- Итого: `31` функций, из них `25` успешно отработали, `6` вернули 502/timeout (ожидаемо проблемные/сломанные кейсы).
- Проверен полный edit flow через UI API на рабочей функции:
- `GET function` -> `PUT /code` -> `POST /invoke` -> `PUT /code` (restore) -> `POST /invoke`.
### Найденный UI-баг и исправление
- Баг: после обновления кода через UI следующий invoke мог отдавать старую специализацию/кэш.
- Причина: обновлялся только `Package`, но не обновлялся `Function.spec.package.packageref.resourceversion`.
- Фикс в `console/main.go`:
- после `package update` читается новый `package.resourceVersion`
- выполняется update `Function` с новым `packageref.resourceversion`
- invoke сразу использует новую (или восстановленную) версию кода.
### Проверка фикса
- После update через UI invoke возвращает новый ответ.
- После restore через UI invoke возвращает исходный ответ (без зависания старого кэша).
### Статус `fn-go-acc`
- `fn-go-acc` продолжает падать не из-за UI, а из-за runtime specialization на стороне Fission.
- Подтверждено логами `router/executor`: `GetServiceForFunction ... context canceled` и постоянными readiness-fail у poolmgr pod-ов `go-acc`.
## 2026-04-15 (дополнение) — Финальный фикс `fn-go-acc`
### Симптом и root cause
- `fn-go-acc` стабильно timeout'ился на invoke.
- Изначальный пакет содержал `main.go` как `deployment.literal`, из-за чего go-runtime пытался грузить текст как plugin:
- `plugin.Open("/userfunc/deployarchive/main.go"): invalid ELF header`.
### Что сделано
- Пересобран deploy-артефакт как Go plugin (`main.so`) и упакован в `deploy.zip`.
- Важно: сборка выполнена в том же образе, что у Fission environment builder:
- `ghcr.io/fission/go-builder` (Go 1.25.6), чтобы избежать несовместимости plugin ABI.
- Функция обновлена через Fission CLI:
- `fission function update -n default --name fn-go-acc --env go-acc --entrypoint Handler --deployarchive /tmp/fn-go-acc-fix2/deploy.zip -f`.
### Результат проверки
- `/go-acc` через router с JWT: `hello-go-ok`, `HTTP 200`.
- Executor логи: specialization проходит успешно (`specialized pod`, `added function service`), без `invalid ELF`.
### Контрольный smoke после фикса
- `/auto/ok` -> `HTTP 200`
- `/js-acc` -> `HTTP 200`
- `/js-direct` -> `HTTP 200`
- `/go-acc` -> `HTTP 200`
## 2026-04-15 (дополнение) — Edit Code для `fn-go-acc` снова показывает исходник
### Проблема
- В `Edit Code` для `fn-go-acc` поле `code` было пустым.
- Причина: после `--deployarchive` пакет `pkg-go-acc` перешел на `spec.deployment.type=url`, а в console backend чтение кода шло только из `spec.deployment.literal`.
### Исправление
- В `console/main.go` добавлен fallback-поиск исходника:
- `spec.source.literal`
- `spec.deployment.literal`
- `spec.source.url`
- `spec.deployment.url`
- Добавлена загрузка архива по URL и извлечение исходника из zip (если есть текстовые файлы).
- Добавлен unit-тест `TestGetFunctionUsesSourceLiteralWhenDeploymentLiteralMissing`.
- Собран и выкачен образ `naeel/fission-console:v0.2.6`, деплой обновлен.
### Дополнительно по данным в кластере
- Для `fn-go-acc` обновлен `sourcearchive` (`main.go`), чтобы `pkg-go-acc.spec.source.literal` содержал исходник и был доступен в Edit Code.
### Проверка
- `GET /console/api/functions/fn-go-acc` возвращает непустой `code` (Go source).
- `/go-acc` продолжает отвечать `hello-go-ok`, `HTTP 200`.
## 2026-04-15 (дополнение) — TLS в console, бренд NUBES, проверка `tf-neg-syntax-fn`
### TLS: фактический статус
- `http://fission.kube5s.ru/console/` -> `308 Permanent Redirect` на `https://...`
- `https://fission.kube5s.ru/console/` -> `200`
- Ingress `fission-console` настроен с:
- `nginx.ingress.kubernetes.io/force-ssl-redirect: "true"`
- `tls.secretName: fission-tls`
- `cert-manager.io/cluster-issuer: letsencrypt-prod`
### UI оформление (NUBES)
- В `console/ui/index.html` добавлены:
- favicon (inline SVG)
- брендинг в navbar: mark + wordmark `NUBES` / `FISSION CONSOLE`
- обновлен `<title>` на `NUBES Fission Console`
- Выкат: `naeel/fission-console:v0.2.9`.
### Invoke `tf-neg-syntax-fn` — проверка
- Подтверждено, что у функции синтаксически невалидный код (`def main(: ...`).
- Console invoke теперь возвращает fail-fast ошибку (не «молчание»):
- `{"error":"invoke \"tf-neg-syntax-fn\" timeout after 20s: function specialization likely failed (for example, syntax error)"}`
- Причина на кластере: specialization падает в runtime с `500`, executor делает ретраи.
## 2026-04-15 (дополнение) — Лечение "красного" HTTPS
### Root cause
- На одном host `fission.kube5s.ru` было два ingress:
- `fission-console` с TLS
- `fission-router` без TLS
- Из-за смешанной host-конфигурации TLS мог работать нестабильно/давать красный индикатор в браузере.
### Fix
- Пропатчен `fission/fission-router`:
- добавлен `spec.tls` с `secretName: fission-tls`
- добавлена аннотация `nginx.ingress.kubernetes.io/force-ssl-redirect: "true"`
### Проверка
- `http://fission.kube5s.ru/neg/syntax` -> `308` redirect на HTTPS
- Сертификат endpoint: Let's Encrypt R13, CN/SAN `fission.kube5s.ru`, валиден
- `https://fission.kube5s.ru/console/` -> `HTTP 200`
## 2026-04-15 (дополнение) — Строгая очистка: оставлены только рабочие и с видимым кодом
### Требование
- Убрать всё лишнее и оставить только функции, которые одновременно:
- имеют непустой `code` в `GET /console/api/functions/{name}`
- успешно проходят `POST /console/api/functions/{name}/invoke` со `status=200`
### Что сделано
- Выполнена финальная очистка функций по whitelist.
- После очистки дополнительно проверены все оставшиеся функции через Console API (code + invoke).
- Исправлена ошибка в служебном cleanup-скрипте (битый jsonpath с `{n}`), из-за которой падала пост-обработка orphan-ресурсов.
### Итоговый набор
- `auth-test2`
- `fn-js-direct`
- `fn-js-acc`
- `hello`
- `tf-auto-ok-fn`
- `tf-stress-fast-fn`
### Финальное состояние кластера
- `functions=6`
- `httptriggers=6`
- `packages=6`
### Финальная валидация
- Для каждой из 6 функций: `code=OK`, `invoke=OK`.
## 2026-04-15 (дополнение) — Наращивание до 15 функций + исправление фавикона в live UI
### Запрос
- Увеличить набор до 15 функций, включая error-кейсы.
- Прогнать тесты и чинить код/манифесты при расхождении с ожиданием.
- Исправить favicon в реальном UI (а не только в репозитории).
### Что сделано по функциям
- Подняты функции из `examples`: `hello-python`, `deep-recursion`, `destroy-test`, `frequent-update`, `orphan-test`, `multi-env-1/2/3`.
- Добавлен негативный кейс `tf-neg-syntax-fn` с route `/neg/syntax`.
- Доведено до ровно `functions=15`, `httptriggers=15`.
### Фиксы по коду/манифестам (по результатам тестов)
- `examples/multi-env-1/main.tf`
- исправлен `source_dir` с `"$\{path.module\}/code"` на `"${path.module}/code"`.
- заменен image env с `ghcr.io/fission/python-env:v1.20.0` на `ghcr.io/fission/python-env`.
- `examples/multi-env-2/main.tf`
- аналогичные исправления `source_dir` и image.
- `examples/multi-env-3/main.tf`
- аналогичные исправления `source_dir` и image.
### Тест-матрица (router + JWT)
- Итог: `PASS 15 / FAIL 0`.
- Успешные (`HTTP 200`):
- `/auth-test2`, `/js-acc`, `/js-direct`, `/hello`, `/auto/ok`, `/destroy-test`, `/freq-update`, `/tf-hello`, `/multi-env-1`, `/multi-env-2`, `/multi-env-3`, `/orphan-test`, `/stress/fast`.
- Ожидаемые error-кейсы:
- `/deep-recursion` -> timeout (`curl rc=28`, `HTTP 000`)
- `/neg/syntax` -> timeout (`curl rc=28`, `HTTP 000`)
- Доп. проверка через Console invoke:
- `tf-multi-env-1-fn` -> `status=200`
- `tf-hello-fn` -> `status=200`
- `tf-neg-syntax-fn` и `tf-deep-recursion-fn` -> ожидаемая fail-fast ошибка invoke timeout.
### Фавикон (live)
- Причина «старого фавикона»: в кластере работал старый образ `naeel/fission-console:v0.3.0`.
- Собран и выкачен новый образ: `naeel/fission-console:v0.3.1`.
- Deployment обновлен и успешно прокатан.
- Проверено в live HTML: отдается
- `<link rel="icon" type="image/png" href="https://nubes.ru/themes/custom/nubes_2025/favicon.png">`.
+271
View File
@@ -147,3 +147,274 @@ User → POST /console/api/functions/NAME/invoke
Причина: VPN может быть активен локально, вызывая сбои при прямом выполнении команд. Причина: VPN может быть активен локально, вызывая сбои при прямом выполнении команд.
Монтирование sshfs делает локальное редактирование файлов эквивалентным редактированию на ВМ. Монтирование sshfs делает локальное редактирование файлов эквивалентным редактированию на ВМ.
---
## Сессия: JS invoke timeouts в UI (root cause + fix)
### Симптом
- Все JS invoke через UI (`/console/api/functions/{name}/invoke`) падали по timeout.
- В router логах: `roundtripper timeout (60s)`/`error sending request to function`.
- В executor логах specialization для node окружения могла проходить, но ответ функции не возвращался.
### Ключевые находки
1. Node runtime работал через `POST /v2/specialize`.
2. При `functionName=main` runtime строил путь загрузки как:
- `/userfunc/deployarchive/main`
3. В реальном поде `/userfunc/deployarchive` был **файлом** (literal payload), а не директорией.
4. После перевода на пустой entrypoint (`functionName=""`) specialization стала успешной (`202`, `user code loaded`),
но запросы всё равно зависали.
5. Прямая проверка контейнера показала второй контрактный нюанс node runtime:
- пользовательская функция должна возвращать объект формата
`{ status, body, headers? }`
- возврат простой строки приводил к зависанию ответа (callback не отправлял HTTP response).
### Принятые изменения
- `nodejs-acc` закреплен на `ghcr.io/fission/node-env:1.32.5`.
- Для JS функций (`fn-js-acc`, `fn-js-direct`, тестовые `jsm1..jsm4`):
- выставлен пустой entrypoint (`functionName=""`)
- код приведен к контракту Node runtime:
- `module.exports = async function(context) { return { status: 200, body: "..." }; }`
- Для основных JS trigger включены методы `GET` и `POST`.
### Проверка
- UI invoke:
- `fn-js-acc` -> `status=200`, `response_raw=hello-js-ok`
- `fn-js-direct` -> `status=200`, `response_raw=hello-js-ok`
- Прямые GET через роутер:
- `/js-acc` -> 200
- `/js-direct` -> 200
- Тестовые `jsm1..jsm4` после унификации кода -> 200 через UI invoke.
### Вывод
Проблема была не в UI-рендеринге и не в одном конкретном trigger, а в несовпадении контракта Node runtime:
- entrypoint/path specialization
- shape возвращаемого значения функции.
---
## Сессия: Финальный fix `fn-go-acc` (Go runtime specialization)
### Симптом
- `fn-go-acc` выдавал timeout на invoke через console/router.
### Диагностика
1. На исходной конфигурации в executor логах:
- `plugin.Open("/userfunc/deployarchive/main.go"): invalid ELF header`.
2. Это означало, что runtime ожидал plugin-артефакт, но в package deployment лежал исходник `main.go`.
3. После первой попытки собрать plugin локальным `go1.26.1` ошибка `invalid ELF` ушла, но появился новый фейл specialization:
- `Post "http://127.0.0.1:8888/v2/specialize": EOF`.
4. Вывод: plugin собран несовместимой версией Go относительно runtime.
### Решение
- Сборка plugin выполнена в `ghcr.io/fission/go-builder` (Go 1.25.6), то есть тем же toolchain, что у Fission environment builder:
- `GO111MODULE=off /usr/local/go/bin/go build -buildmode=plugin -o main.so ./main.go`
- упаковка `main.so` в `deploy.zip`.
- Обновление функции через CLI:
- `fission function update -n default --name fn-go-acc --env go-acc --entrypoint Handler --deployarchive /tmp/fn-go-acc-fix2/deploy.zip -f`
### Проверка
- `/go-acc` через router с JWT: `hello-go-ok`, `HTTP 200`.
- В executor логах specialization успешен:
- `specialized pod`
- `added function service`
- без `invalid ELF` и без `EOF`.
### Контрольная матрица маршрутов
- `/auto/ok` -> 200
- `/js-acc` -> 200
- `/js-direct` -> 200
- `/go-acc` -> 200
### Практический вывод
Для go-env в этом кластере критично соблюдать контракт runtime:
1. deploy-пакет должен содержать именно plugin (`.so`), а не исходник.
2. plugin должен быть собран совместимой версией Go (через `ghcr.io/fission/go-builder`).
---
## Сессия: `Edit Code` для `fn-go-acc` пустой
### Симптом
- В UI (`GET /console/api/functions/fn-go-acc`) поле `code` было пустым.
### Почему так произошло
- После фикса runtime функция была переведена на deploy-архив plugin (`main.so`).
- Package имел вид:
- `spec.deployment.type=url`
- `spec.deployment.url=...`
- без `spec.deployment.literal`
- Backend в `handleGetFunction` извлекал код только из `spec.deployment.literal`, поэтому для URL-пакета отдавал пустую строку.
### Что изменено
- В backend добавлен `extractPackageSourceCode(...)` с fallback-приоритетом:
1. `spec.source.literal`
2. `spec.deployment.literal`
3. `spec.source.url`
4. `spec.deployment.url`
- Добавлена загрузка URL-архива через `fetchPackageArchive(...)` и декодирование через `decodeArchiveBytesToSource(...)`.
- Обновлен unit-тестами сценарий, где `deployment.literal` отсутствует, но есть `source.literal`.
### Операционные шаги
- Собран и задеплоен `naeel/fission-console:v0.2.6`.
- Для текущей `fn-go-acc` дополнительно обновлен `sourcearchive` (`main.go`), чтобы исходник гарантированно отображался в Edit Code.
### Результат
- `GET /console/api/functions/fn-go-acc` теперь возвращает непустой Go source в `code`.
- Вызов `/go-acc` остается рабочим (`hello-go-ok`, HTTP 200).
---
## Сессия: Проверка TLS + branding NUBES + верификация `tf-neg-syntax-fn`
### TLS
- Проверен доступ к console:
- `http://.../console/` -> `308` redirect на HTTPS
- `https://.../console/` -> `200`
- Ingress конфиг содержит корректный TLS блок и принудительный SSL redirect.
- Вывод: endpoint защищен TLS, «незащищенного HTTP-доступа» нет.
### Брендинг NUBES
- В `console/ui/index.html` добавлены:
- `<title>NUBES Fission Console</title>`
- favicon (inline SVG)
- navbar brand block с `NUBES` / `FISSION CONSOLE`.
- Обновлен образ до `naeel/fission-console:v0.2.9`.
### `tf-neg-syntax-fn` — почему timeout вместо прямого SyntaxError
- Функция содержит заведомо битый Python (`def main(:`), confirmed через `GET /console/api/functions/tf-neg-syntax-fn`.
- На runtime это роняет specialization (`500`), после чего executor ретраит.
- Поэтому router/console не получает «чистый traceback» сразу из runtime API и видит timeout/ошибку specialization.
- Для UX введен fail-fast и явная ошибка в console invoke:
- `timeout after 20s: function specialization likely failed (for example, syntax error)`.
---
## Сессия: "Не защищено красным" — экстренное лечение
### Наблюдение
- Сертификат endpoint сам по себе валидный (Let's Encrypt, CN/SAN = `fission.kube5s.ru`).
- Но на том же host были два ingress с разной TLS-конфигурацией:
- `fission-console` с TLS
- `fission-router` без TLS
### Действие
- Патч `fission-router`:
- добавлен `spec.tls` с `secretName: fission-tls`
- добавлен `force-ssl-redirect=true`
### Результат
- HTTP на роутер-пути дает 308 -> HTTPS
- HTTPS на console стабильно 200
- Конфигурация host выровнена: теперь и console, и router на одном сертификате.
---
## Сессия: Строгая чистка до "только точно рабочие и видимые"
### Запрос
- Оставить только функции, которые в UI/Console:
- показывают исходник (`code` не пустой)
- реально вызываются (`invoke.status == 200`)
### Что произошло
- Выполнен массовый cleanup с keep-list.
- По логу cleanup выявилась ошибка jsonpath в пост-обработке orphan-пакетов:
- использовалось `{n}` вместо `{"\\n"}`
- это ломало шаг удаления orphan-объектов после основного удаления.
- Основное удаление функций при этом сработало корректно.
### Проверка после cleanup
- Текущее состояние:
- functions: 6
- httptriggers: 6
- packages: 6
- Остались только:
- `auth-test2`
- `fn-js-direct`
- `fn-js-acc`
- `hello`
- `tf-auto-ok-fn`
- `tf-stress-fast-fn`
- Для каждой функции через Console API подтверждено:
- `code=OK`
- `invoke=OK`
### Вывод
- Финальный набор теперь соответствует строгому критерию "однозначно работает и показывается".
---
## Сессия: Возврат к 15 функциям + проверка ожиданий + live favicon
### Наблюдение в начале
- После строгой чистки в кластере оставалось 6 функций.
- В live UI favicon оставался старым, хотя в репозитории правка уже была.
- Проверка deployment показала: работал старый image `naeel/fission-console:v0.3.0`.
### Расширение набора до 15
- Добавлены функции из примеров:
- `tf-hello-fn`, `tf-deep-recursion-fn`, `tf-destroy-test-fn`, `tf-freq-update-fn`, `tf-orphan-fn`
- `tf-multi-env-1-fn`, `tf-multi-env-2-fn`, `tf-multi-env-3-fn`
- Добавлен негативный кейс:
- `tf-neg-syntax-fn` + route `/neg/syntax`
- Итоговый размер:
- functions = 15
- httptriggers = 15
### Что сломалось и как починено
1. `multi-env-1/2/3` не применялись:
- ошибка Terraform: `Invalid escape sequence` из-за `"$\{path.module\}/code"`.
- фикс: заменено на `"${path.module}/code"` во всех трех `main.tf`.
2. После применения `multi-env-1/2/3` маршруты таймаутили (`rc=28`):
- причина: image env `ghcr.io/fission/python-env:v1.20.0`.
- фикс: переключено на `ghcr.io/fission/python-env`, re-apply всех трех модулей.
- результат: `/multi-env-1`, `/multi-env-2`, `/multi-env-3` -> `HTTP 200`.
### Финальная тест-матрица
- Router tests с JWT: `PASS=15, FAIL=0`.
- Успешные 200: базовые + multi-env + tf-hello + stress-fast.
- Ожидаемые ошибки:
- `/deep-recursion` -> timeout (`HTTP 000`, `rc=28`)
- `/neg/syntax` -> timeout (`HTTP 000`, `rc=28`)
- Console invoke подтверждает:
- positive функции -> `status=200`
- negative функции -> fail-fast timeout error.
### Favicon в live
- Собран и выкачен `naeel/fission-console:v0.3.1`.
- `deployment/fission-console` обновлен и rollout успешен.
- Проверка `https://fission.kube5s.ru/console/` показывает нужный favicon URL:
- `https://nubes.ru/themes/custom/nubes_2025/favicon.png`.
+1 -1
View File
@@ -1,2 +1,2 @@
def main(): def main():
return "ok-auto-func-UPDATED-v2" return "ok-auto-func-UPDATED-v3-with-comment"
+26
View File
@@ -0,0 +1,26 @@
package main
import "fmt"
// Handler используется binary-env: binary получает HTTP request через stdin,
// stdout является HTTP response body.
//
// Для деплоя нужно скомпилировать перед terraform apply:
// CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o ../dist/handler .
//
// Compiled size: ~1.6MB (слишком велико для literal deployment).
// Используется shell-based handler в dist/handler (см. ниже).
func main() {
fmt.Print("Hello from Go in Fission")
}
package main
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Hello from Go in Fission")
}
+42
View File
@@ -0,0 +1,42 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
# Go использует binary-env: функция является скомпилированным Linux-бинарником.
# Перед apply необходимо скомпилировать:
# CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o dist/handler code/
resource "fission_environment" "go" {
name = "tf-go-hello-env"
image = "ghcr.io/fission/binary-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-go-hello-pkg"
environment = fission_environment.go.name
code_path = "${path.module}/dist/handler"
}
resource "fission_function" "fn" {
name = "tf-go-hello-fn"
environment = fission_environment.go.name
package_name = fission_package.pkg.name
entrypoint = "handler"
}
resource "fission_http_trigger" "route" {
name = "tf-go-hello-route"
function = fission_function.fn.name
url = "/go-hello"
methods = ["GET"]
}
+39
View File
@@ -0,0 +1,39 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "python" {
name = "tf-multi-env-1"
image = "ghcr.io/fission/python-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-multi-env-1-pkg"
environment = fission_environment.python.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-multi-env-1-fn"
environment = fission_environment.python.name
package_name = fission_package.pkg.name
entrypoint = "main.main"
}
resource "fission_http_trigger" "route" {
name = "tf-multi-env-1-route"
function = fission_function.fn.name
url = "/multi-env-1"
methods = ["GET"]
}
+39
View File
@@ -0,0 +1,39 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "python" {
name = "tf-multi-env-2"
image = "ghcr.io/fission/python-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-multi-env-2-pkg"
environment = fission_environment.python.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-multi-env-2-fn"
environment = fission_environment.python.name
package_name = fission_package.pkg.name
entrypoint = "main.main"
}
resource "fission_http_trigger" "route" {
name = "tf-multi-env-2-route"
function = fission_function.fn.name
url = "/multi-env-2"
methods = ["GET"]
}
+39
View File
@@ -0,0 +1,39 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "python" {
name = "tf-multi-env-3"
image = "ghcr.io/fission/python-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-multi-env-3-pkg"
environment = fission_environment.python.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-multi-env-3-fn"
environment = fission_environment.python.name
package_name = fission_package.pkg.name
entrypoint = "main.main"
}
resource "fission_http_trigger" "route" {
name = "tf-multi-env-3-route"
function = fission_function.fn.name
url = "/multi-env-3"
methods = ["GET"]
}
+3
View File
@@ -0,0 +1,3 @@
sub {
return "Hello from Perl in Fission";
}
+40
View File
@@ -0,0 +1,40 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
# version = 1: Perl env uses v1 specialization (/specialize endpoint)
resource "fission_environment" "perl" {
name = "tf-perl-hello-env"
image = "ghcr.io/fission/perl-env"
version = 1
}
resource "fission_package" "pkg" {
name = "tf-perl-hello-pkg"
environment = fission_environment.perl.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-perl-hello-fn"
environment = fission_environment.perl.name
package_name = fission_package.pkg.name
entrypoint = "handler"
}
resource "fission_http_trigger" "route" {
name = "tf-perl-hello-route"
function = fission_function.fn.name
url = "/perl-hello"
methods = ["GET"]
}
+7
View File
@@ -0,0 +1,7 @@
<?php
function handler($context)
{
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = $context["response"];
$response->getBody()->write("Hello from PHP in Fission");
}
+39
View File
@@ -0,0 +1,39 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "php" {
name = "tf-php-hello-env"
image = "ghcr.io/fission/php-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-php-hello-pkg"
environment = fission_environment.php.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-php-hello-fn"
environment = fission_environment.php.name
package_name = fission_package.pkg.name
entrypoint = "main.php::handler"
}
resource "fission_http_trigger" "route" {
name = "tf-php-hello-route"
function = fission_function.fn.name
url = "/php-hello"
methods = ["GET"]
}
+5
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
def handler
"Hello from Ruby in Fission"
end
+39
View File
@@ -0,0 +1,39 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "ruby" {
name = "tf-ruby-hello-env"
image = "ghcr.io/fission/ruby-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-ruby-hello-pkg"
environment = fission_environment.ruby.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-ruby-hello-fn"
environment = fission_environment.ruby.name
package_name = fission_package.pkg.name
entrypoint = "handler"
}
resource "fission_http_trigger" "route" {
name = "tf-ruby-hello-route"
function = fission_function.fn.name
url = "/ruby-hello"
methods = ["GET"]
}
@@ -290,9 +290,16 @@ func unstructuredToFunctionModel(functionObject *unstructured.Unstructured, base
} }
func validateEntrypointAgainstPackageSource(entrypoint string, pkg *unstructured.Unstructured) error { func validateEntrypointAgainstPackageSource(entrypoint string, pkg *unstructured.Unstructured) error {
if entrypoint == "" {
return fmt.Errorf("entrypoint не может быть пустым")
}
// Для Python/Go/JS: валидируем формат module.function и наличие функции в исходнике.
// Для PHP (module::function), Ruby (function), Perl (function) — допускаем любой непустой формат.
parts := strings.Split(entrypoint, ".") parts := strings.Split(entrypoint, ".")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" { if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("entrypoint %q должен иметь формат module.function", entrypoint) // Не стандартный module.function — допускаем (PHP, Ruby, Perl и др.)
return nil
} }
if parts[0] != "main" { if parts[0] != "main" {
@@ -70,7 +70,12 @@ func TestValidateEntrypointAgainstPackageSourcePythonMissing(t *testing.T) {
func TestValidateEntrypointAgainstPackageSourceBadFormat(t *testing.T) { func TestValidateEntrypointAgainstPackageSourceBadFormat(t *testing.T) {
pkg := &unstructured.Unstructured{} pkg := &unstructured.Unstructured{}
if err := validateEntrypointAgainstPackageSource("main", pkg); err == nil { // Пустой entrypoint должен быть ошибкой
t.Fatalf("expected validation error for bad entrypoint format") if err := validateEntrypointAgainstPackageSource("", pkg); err == nil {
t.Fatalf("expected validation error for empty entrypoint")
}
// Одиночное слово допустимо (Ruby, Perl)
if err := validateEntrypointAgainstPackageSource("handler", pkg); err != nil {
t.Fatalf("unexpected error for single-word entrypoint: %v", err)
} }
} }
@@ -71,7 +71,7 @@ func (r *PackageResource) Schema(_ context.Context, _ resource.SchemaRequest, re
}, },
"source_dir": schema.StringAttribute{ "source_dir": schema.StringAttribute{
Optional: true, Optional: true,
Description: "Путь к директории с кодом (поддерживаются main.py, main.js, main.go).", Description: "Путь к директории с кодом (поддерживаются main.py, main.js, main.go, main.php, main.rb, main.pl).",
}, },
"code_path": schema.StringAttribute{ "code_path": schema.StringAttribute{
Optional: true, Optional: true,
@@ -355,7 +355,7 @@ func loadPackageLiteral(sourceDir, codePath string) ([]byte, error) {
// resolveMainSourceFile выбирает основной файл исходника из source_dir. // resolveMainSourceFile выбирает основной файл исходника из source_dir.
func resolveMainSourceFile(sourceDir string) (string, error) { func resolveMainSourceFile(sourceDir string) (string, error) {
candidates := []string{"main.py", "main.js", "main.go"} candidates := []string{"main.py", "main.js", "main.go", "main.php", "main.rb", "main.pl"}
for _, candidate := range candidates { for _, candidate := range candidates {
candidatePath := filepath.Join(sourceDir, candidate) candidatePath := filepath.Join(sourceDir, candidate)
fileInfo, err := os.Stat(candidatePath) fileInfo, err := os.Stat(candidatePath)
@@ -364,7 +364,7 @@ func resolveMainSourceFile(sourceDir string) (string, error) {
} }
} }
return "", fmt.Errorf("source_dir %q must contain one of: main.py, main.js, main.go", sourceDir) return "", fmt.Errorf("source_dir %q must contain one of: main.py, main.js, main.go, main.php, main.rb, main.pl", sourceDir)
} }
// packageToUnstructured преобразует Terraform model в Kubernetes CRD payload. // packageToUnstructured преобразует Terraform model в Kubernetes CRD payload.