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:
@@ -37,16 +37,8 @@ func main() {
|
|||||||
log.Fatalf("create dynamic client: %v", err)
|
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{
|
authenticator := &auth.MultiAuthenticator{
|
||||||
JWT: jwtAuth,
|
JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil),
|
||||||
Demo: &auth.DemoAuthenticator{},
|
Demo: &auth.DemoAuthenticator{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +51,7 @@ func main() {
|
|||||||
SATokenPath: envDefault("SA_TOKEN_PATH", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
SATokenPath: envDefault("SA_TOKEN_PATH", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
||||||
AuthUser: envDefault("FISSION_AUTH_USERNAME", ""),
|
AuthUser: envDefault("FISSION_AUTH_USERNAME", ""),
|
||||||
AuthPass: envDefault("FISSION_AUTH_PASSWORD", ""),
|
AuthPass: envDefault("FISSION_AUTH_PASSWORD", ""),
|
||||||
TestMode: testMode,
|
StoragesvcURL: envDefault("FISSION_STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local"),
|
||||||
Authenticator: authenticator,
|
Authenticator: authenticator,
|
||||||
// --- ai/ask feature ---
|
// --- ai/ask feature ---
|
||||||
LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"),
|
LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v1.3.27
|
image: naeel/fission-console:v1.3.31
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
@@ -77,6 +77,8 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: router
|
name: router
|
||||||
key: password
|
key: password
|
||||||
|
- name: FISSION_STORAGESVC_URL
|
||||||
|
value: "http://storagesvc.fission.svc.cluster.local"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
|
|||||||
@@ -50,28 +50,13 @@ func normalizeEnv(env string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// authMiddleware оборачивает handler аутентификацией.
|
// authMiddleware оборачивает handler аутентификацией.
|
||||||
//
|
// Токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
|
||||||
// В testMode: заголовок X-Test-Sub позволяет задать sub напрямую (без токена).
|
|
||||||
// Иначе: токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
|
|
||||||
func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
|
func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var identity auth.UserIdentity
|
var identity auth.UserIdentity
|
||||||
|
|
||||||
env := normalizeEnv(r.Header.Get("X-Auth-Env"))
|
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)
|
token := authTokenFromRequest(r)
|
||||||
var err error
|
var err error
|
||||||
identity, err = s.authenticator.Authenticate(r.Context(), token, env)
|
identity, err = s.authenticator.Authenticate(r.Context(), token, env)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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 {
|
func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
|
||||||
if fn != nil {
|
if fn != nil {
|
||||||
seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
|
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 в зависимости от языка:
|
// Строим Package spec в зависимости от языка:
|
||||||
// - Go: source package → builder job компилирует в .so плагин
|
// - Go: source package → builder job компилирует в .so плагин
|
||||||
// - Node.js: deployment zip с ESM wrapper (package.json + main.js)
|
// - Node.js: deployment zip с ESM wrapper (package.json + main.js)
|
||||||
// - Остальные: deployment literal с кодом напрямую
|
// - Остальные: deployment archive с кодом (S3 или literal fallback)
|
||||||
var pkgSpec map[string]any
|
var pkgSpec map[string]any
|
||||||
if req.Language == "go" {
|
if req.Language == "go" {
|
||||||
srcZip, err := runtime.BuildGoSourceZip(req.Code)
|
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))
|
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err))
|
||||||
return
|
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{
|
pkgSpec = map[string]any{
|
||||||
"source": map[string]any{
|
"source": srcSpec,
|
||||||
"type": "literal",
|
|
||||||
"literal": literal,
|
|
||||||
},
|
|
||||||
"deployment": map[string]any{},
|
"deployment": map[string]any{},
|
||||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||||
"buildcommand": "build",
|
"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))
|
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr))
|
||||||
return
|
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{
|
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},
|
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||||
"source": map[string]any{},
|
"source": map[string]any{},
|
||||||
}
|
}
|
||||||
@@ -563,12 +666,17 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
|
|||||||
}()
|
}()
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36)
|
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{
|
newPkg := &unstructured.Unstructured{Object: map[string]any{
|
||||||
"apiVersion": "fission.io/v1",
|
"apiVersion": "fission.io/v1",
|
||||||
"kind": "Package",
|
"kind": "Package",
|
||||||
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
|
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
|
||||||
"spec": map[string]any{
|
"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},
|
"environment": map[string]any{"name": envName, "namespace": ns},
|
||||||
"source": map[string]any{},
|
"source": map[string]any{},
|
||||||
},
|
},
|
||||||
@@ -1000,10 +1108,22 @@ func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, na
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pkgName != "" {
|
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) {
|
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))
|
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Удаляем архив из S3 после успешного удаления Package (best-effort)
|
||||||
|
if archiveURL != "" {
|
||||||
|
go s.deleteFromStoragesvc(context.Background(), archiveURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Убираем environment pool pods если язык больше не используется (best-effort)
|
// Убираем environment pool pods если язык больше не используется (best-effort)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -36,10 +37,10 @@ type Server struct {
|
|||||||
|
|
||||||
saTokenPath string
|
saTokenPath string
|
||||||
invokeTimeout time.Duration
|
invokeTimeout time.Duration
|
||||||
testMode bool // FISSION_TEST_MODE=true — разрешает X-Test-Sub shortcut
|
|
||||||
|
|
||||||
authUser string
|
authUser string
|
||||||
authPass string
|
authPass string
|
||||||
|
storagesvcURL string // URL Fission storagesvc для загрузки архивов кода в S3
|
||||||
|
|
||||||
// --- ai/ask feature (удалить блок целиком чтобы выкосить) ---
|
// --- ai/ask feature (удалить блок целиком чтобы выкосить) ---
|
||||||
llmURL string // FISSION_LLM_URL
|
llmURL string // FISSION_LLM_URL
|
||||||
@@ -68,7 +69,7 @@ type Config struct {
|
|||||||
SATokenPath string
|
SATokenPath string
|
||||||
AuthUser string
|
AuthUser string
|
||||||
AuthPass string
|
AuthPass string
|
||||||
TestMode bool
|
StoragesvcURL string
|
||||||
Authenticator auth.Authenticator // слой аутентификации
|
Authenticator auth.Authenticator // слой аутентификации
|
||||||
LLMUrl string
|
LLMUrl string
|
||||||
LLMKey string
|
LLMKey string
|
||||||
@@ -76,6 +77,7 @@ type Config struct {
|
|||||||
|
|
||||||
// NewServer создаёт и настраивает HTTP Server со всеми зависимостями.
|
// NewServer создаёт и настраивает HTTP Server со всеми зависимостями.
|
||||||
func NewServer(cfg Config) *Server {
|
func NewServer(cfg Config) *Server {
|
||||||
|
log.Printf("NewServer: storagesvcURL=%q", cfg.StoragesvcURL)
|
||||||
return &Server{
|
return &Server{
|
||||||
dyn: cfg.Dyn,
|
dyn: cfg.Dyn,
|
||||||
ns: cfg.Namespace,
|
ns: cfg.Namespace,
|
||||||
@@ -85,7 +87,7 @@ func NewServer(cfg Config) *Server {
|
|||||||
invokeTimeout: cfg.InvokeTimeout,
|
invokeTimeout: cfg.InvokeTimeout,
|
||||||
authUser: cfg.AuthUser,
|
authUser: cfg.AuthUser,
|
||||||
authPass: cfg.AuthPass,
|
authPass: cfg.AuthPass,
|
||||||
testMode: cfg.TestMode,
|
storagesvcURL: cfg.StoragesvcURL,
|
||||||
authenticator: cfg.Authenticator,
|
authenticator: cfg.Authenticator,
|
||||||
llmURL: cfg.LLMUrl,
|
llmURL: cfg.LLMUrl,
|
||||||
llmKey: cfg.LLMKey,
|
llmKey: cfg.LLMKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user