feat: remove test mode (X-Test-Sub), all functions stored in S3 via storagesvc

- Remove FISSION_TEST_MODE and X-Test-Sub bypass from authMiddleware
- Remove testMode field from Server/Config structs
- Remove TestAuthenticator branch from main.go
- Add uploadToStoragesvc/buildDeploySpec: all function code uploaded to S3
- Add deleteFromStoragesvc: archive deleted from S3 on function delete
- Add FISSION_STORAGESVC_URL env to main.go and deploy/console.yaml
- image: naeel/fission-console:v1.3.31
This commit is contained in:
“Naeel”
2026-05-03 13:37:12 +04:00
parent e8f06d4ab2
commit a09291e208
5 changed files with 141 additions and 40 deletions
+2 -10
View File
@@ -37,16 +37,8 @@ func main() {
log.Fatalf("create dynamic client: %v", err)
}
testMode := os.Getenv("FISSION_TEST_MODE") == "true"
var jwtAuth auth.Authenticator
if testMode {
jwtAuth = &auth.TestAuthenticator{}
} else {
jwtAuth = auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil)
}
authenticator := &auth.MultiAuthenticator{
JWT: jwtAuth,
JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil),
Demo: &auth.DemoAuthenticator{},
}
@@ -59,7 +51,7 @@ func main() {
SATokenPath: envDefault("SA_TOKEN_PATH", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
AuthUser: envDefault("FISSION_AUTH_USERNAME", ""),
AuthPass: envDefault("FISSION_AUTH_PASSWORD", ""),
TestMode: testMode,
StoragesvcURL: envDefault("FISSION_STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local"),
Authenticator: authenticator,
// --- ai/ask feature ---
LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"),
+3 -1
View File
@@ -52,7 +52,7 @@ spec:
serviceAccountName: fission-console
containers:
- name: console
image: naeel/fission-console:v1.3.27
image: naeel/fission-console:v1.3.31
imagePullPolicy: Always
ports:
- containerPort: 8090
@@ -77,6 +77,8 @@ spec:
secretKeyRef:
name: router
key: password
- name: FISSION_STORAGESVC_URL
value: "http://storagesvc.fission.svc.cluster.local"
livenessProbe:
httpGet:
path: /health
+1 -16
View File
@@ -50,28 +50,13 @@ func normalizeEnv(env string) string {
}
// authMiddleware оборачивает handler аутентификацией.
//
// В testMode: заголовок X-Test-Sub позволяет задать sub напрямую (без токена).
// Иначе: токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
// Токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var identity auth.UserIdentity
env := normalizeEnv(r.Header.Get("X-Auth-Env"))
if s.testMode {
if sub := strings.TrimSpace(r.Header.Get("X-Test-Sub")); sub != "" {
identity = auth.UserIdentity{Sub: sub}
ctx := s.contextWithIdentity(r.Context(), identity)
if err := s.nsManager.EnsureUserNS(ctx, auth.NamespaceForSub(sub)); err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", err))
return
}
h(w, r.WithContext(ctx))
return
}
}
token := authTokenFromRequest(r)
var err error
identity, err = s.authenticator.Authenticate(r.Context(), token, env)
+128 -8
View File
@@ -11,6 +11,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
@@ -51,6 +52,102 @@ func buildDeployArchive(lang, code string) ([]byte, error) {
}
}
// deleteFromStoragesvc удаляет архив из S3 через storagesvc по URL из Package spec.
// URL имеет формат: http://storagesvc.../v1/archive?id=fission/UUID
// Best-effort: ошибка логируется, но не прерывает операцию удаления.
func (s *Server) deleteFromStoragesvc(ctx context.Context, archiveURL string) {
if s.storagesvcURL == "" || archiveURL == "" {
return
}
// archiveURL = "http://storagesvc.../v1/archive?id=fission/UUID"
// Строим DELETE URL к storagesvc, сохраняя query-параметр id
parsed, err := url.Parse(archiveURL)
if err != nil {
log.Printf("deleteFromStoragesvc: parse url %q: %v", archiveURL, err)
return
}
deleteURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?" + parsed.RawQuery
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
if err != nil {
log.Printf("deleteFromStoragesvc: build request: %v", err)
return
}
resp, err := s.http.Do(req)
if err != nil {
log.Printf("deleteFromStoragesvc: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
log.Printf("deleteFromStoragesvc: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
return
}
log.Printf("deleteFromStoragesvc: deleted %s", parsed.Query().Get("id"))
}
// uploadToStoragesvc загружает байты в Fission storagesvc и возвращает URL для package archive.
// Если storagesvcURL не задан — возвращает пустую строку (fallback на literal).
func (s *Server) uploadToStoragesvc(ctx context.Context, data []byte) (string, error) {
if s.storagesvcURL == "" {
log.Printf("uploadToStoragesvc: storagesvcURL is empty, skip upload")
return "", nil
}
log.Printf("uploadToStoragesvc: uploading %d bytes to %s", len(data), s.storagesvcURL)
uploadURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive"
body := &bytes.Reader{}
// multipart/form-data с полем uploadfile
var buf bytes.Buffer
boundary := fmt.Sprintf("fission%d", time.Now().UnixNano())
buf.WriteString("--" + boundary + "\r\n")
buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"archive.zip\"\r\n"))
buf.WriteString("Content-Type: application/octet-stream\r\n\r\n")
buf.Write(data)
buf.WriteString("\r\n--" + boundary + "--\r\n")
body = bytes.NewReader(buf.Bytes())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, body)
if err != nil {
return "", fmt.Errorf("build storagesvc upload request: %w", err)
}
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
req.Header.Set("X-File-Size", fmt.Sprintf("%d", len(data)))
resp, err := s.http.Do(req)
if err != nil {
return "", fmt.Errorf("storagesvc upload: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("storagesvc upload status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}
var result struct {
ID string `json:"id"`
}
if err := json.Unmarshal(respBody, &result); err != nil || result.ID == "" {
return "", fmt.Errorf("storagesvc upload: bad response: %s", string(respBody))
}
archiveURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?id=" + result.ID
return archiveURL, nil
}
// buildDeploySpec строит spec.deployment для Fission Package.
// Если storagesvcURL задан — загружает архив в S3 через storagesvc и возвращает type: url.
// Иначе — возвращает type: literal с base64-кодом.
func (s *Server) buildDeploySpec(ctx context.Context, data []byte) (map[string]any, error) {
archiveURL, err := s.uploadToStoragesvc(ctx, data)
if err != nil {
log.Printf("storagesvc upload failed, falling back to literal: %v", err)
// fallback — сохраняем как literal
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
}
if archiveURL == "" {
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
}
return map[string]any{"type": "url", "url": archiveURL}, nil
}
func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
if fn != nil {
seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
@@ -313,7 +410,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
// Строим Package spec в зависимости от языка:
// - Go: source package → builder job компилирует в .so плагин
// - Node.js: deployment zip с ESM wrapper (package.json + main.js)
// - Остальные: deployment literal с кодом напрямую
// - Остальные: deployment archive с кодом (S3 или literal fallback)
var pkgSpec map[string]any
if req.Language == "go" {
srcZip, err := runtime.BuildGoSourceZip(req.Code)
@@ -321,12 +418,13 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err))
return
}
literal := base64.StdEncoding.EncodeToString(srcZip)
srcSpec, srcErr := s.buildDeploySpec(ctx, srcZip)
if srcErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload go source: %v", srcErr))
return
}
pkgSpec = map[string]any{
"source": map[string]any{
"type": "literal",
"literal": literal,
},
"source": srcSpec,
"deployment": map[string]any{},
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"buildcommand": "build",
@@ -337,8 +435,13 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr))
return
}
deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes)
if uploadErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", req.Language, uploadErr))
return
}
pkgSpec = map[string]any{
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
"deployment": deploySpec,
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"source": map[string]any{},
}
@@ -563,12 +666,17 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
}()
now := time.Now().UTC()
newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36)
deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes)
if uploadErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", lang, uploadErr))
return
}
newPkg := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Package",
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
"spec": map[string]any{
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
"deployment": deploySpec,
"environment": map[string]any{"name": envName, "namespace": ns},
"source": map[string]any{},
},
@@ -1000,10 +1108,22 @@ func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, na
}
if pkgName != "" {
// Получаем URL архива из Package spec.deployment перед удалением, чтобы потом очистить S3
var archiveURL string
if pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, pkgName, metav1.GetOptions{}); pkgErr == nil {
deployType, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "type")
if deployType == "url" {
archiveURL, _, _ = unstructured.NestedString(pkg.Object, "spec", "deployment", "url")
}
}
if err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err))
return
}
// Удаляем архив из S3 после успешного удаления Package (best-effort)
if archiveURL != "" {
go s.deleteFromStoragesvc(context.Background(), archiveURL)
}
}
// Убираем environment pool pods если язык больше не используется (best-effort)
+7 -5
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
@@ -36,10 +37,10 @@ type Server struct {
saTokenPath string
invokeTimeout time.Duration
testMode bool // FISSION_TEST_MODE=true — разрешает X-Test-Sub shortcut
authUser string
authPass string
authUser string
authPass string
storagesvcURL string // URL Fission storagesvc для загрузки архивов кода в S3
// --- ai/ask feature (удалить блок целиком чтобы выкосить) ---
llmURL string // FISSION_LLM_URL
@@ -68,7 +69,7 @@ type Config struct {
SATokenPath string
AuthUser string
AuthPass string
TestMode bool
StoragesvcURL string
Authenticator auth.Authenticator // слой аутентификации
LLMUrl string
LLMKey string
@@ -76,6 +77,7 @@ type Config struct {
// NewServer создаёт и настраивает HTTP Server со всеми зависимостями.
func NewServer(cfg Config) *Server {
log.Printf("NewServer: storagesvcURL=%q", cfg.StoragesvcURL)
return &Server{
dyn: cfg.Dyn,
ns: cfg.Namespace,
@@ -85,7 +87,7 @@ func NewServer(cfg Config) *Server {
invokeTimeout: cfg.InvokeTimeout,
authUser: cfg.AuthUser,
authPass: cfg.AuthPass,
testMode: cfg.TestMode,
storagesvcURL: cfg.StoragesvcURL,
authenticator: cfg.Authenticator,
llmURL: cfg.LLMUrl,
llmKey: cfg.LLMKey,