revert: remove Go function (runtime compilation timeout)

This commit is contained in:
Naeel
2026-04-15 16:24:57 +03:00
parent 66404fde3a
commit cccc5ca024
5 changed files with 198 additions and 72 deletions
+126 -21
View File
@@ -6,9 +6,11 @@ import (
"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" "sort"
@@ -39,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
@@ -71,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 {
@@ -87,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()
@@ -126,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,
} }
@@ -341,12 +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 != "" {
if decodedCode, decErr := decodeLiteralToSource(literal); decErr == nil {
code = decodedCode
}
}
} }
} }
@@ -459,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)
@@ -516,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
} }
@@ -694,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
@@ -701,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"}
@@ -721,12 +760,78 @@ 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) { func decodeLiteralToSource(literal string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(literal) decoded, err := base64.StdEncoding.DecodeString(literal)
if err != nil { if err != nil {
return "", err return "", err
} }
return decodeArchiveBytesToSource(decoded)
}
func decodeArchiveBytesToSource(decoded []byte) (string, error) {
if len(decoded) == 0 {
return "", fmt.Errorf("empty payload")
}
if utf8.Valid(decoded) { if utf8.Valid(decoded) {
return string(decoded), nil return string(decoded), nil
} }
@@ -737,7 +842,7 @@ func decodeLiteralToSource(literal string) (string, error) {
} }
} }
return string(decoded), nil return "", fmt.Errorf("payload does not contain utf-8 source")
} }
func decodeZipSource(zipBytes []byte) (string, error) { func decodeZipSource(zipBytes []byte) (string, error) {
+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
+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"
-11
View File
@@ -1,11 +0,0 @@
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")
}
-39
View File
@@ -1,39 +0,0 @@
terraform {
required_providers {
fission = {
source = "nail/fission"
version = "~> 0.1.0"
}
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
resource "fission_environment" "go" {
name = "tf-go-hello-env"
image = "ghcr.io/fission/go-env"
version = 3
}
resource "fission_package" "pkg" {
name = "tf-go-hello-pkg"
environment = fission_environment.go.name
source_dir = "${path.module}/code"
}
resource "fission_function" "fn" {
name = "tf-go-hello-fn"
environment = fission_environment.go.name
package_name = fission_package.pkg.name
entrypoint = "main.Handler"
}
resource "fission_http_trigger" "route" {
name = "tf-go-hello-route"
function = fission_function.fn.name
url = "/go-hello"
methods = ["GET"]
}