fix(cron): timer auth chain via console gateway, bump image to v1.3.9 - Added handleFissionFunctionGateway in handlers.go: accepts unauthenticated POST /fission-function/<ns>/<fn>/, fetches router JWT, proxies upstream - Registered public routes /fission-function and /fission-function/ in server.go - Bumped image tag v1.3.8 -> v1.3.9 in deploy/console.yaml - Live: patched timer Deployment --routerUrl to http://fission-console.fission.svc.cluster.local:8090 - Added doc/cron/CRON_TIMER_FIX_2026-04-29.md Root cause: timer reads --routerUrl CLI arg only (not FISSION_ROUTER_URL env), router.fission requires JWT that timer does not provide -> 401 on every tick.
This commit is contained in:
@@ -739,6 +739,114 @@ func buildInternalInvokeURL(routerURL, namespace, functionName string) string {
|
||||
return fmt.Sprintf("%s/fission-function/%s/%s", routerURL, namespace, functionName)
|
||||
}
|
||||
|
||||
// handleFissionFunctionGateway принимает внутренние invoke-запросы timer/router
|
||||
// и проксирует их через console в upstream router с корректным router JWT.
|
||||
func (s *Server) handleFissionFunctionGateway(w http.ResponseWriter, r *http.Request) {
|
||||
rawPath := strings.Trim(strings.TrimPrefix(r.URL.Path, "/fission-function"), "/")
|
||||
if rawPath == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(rawPath, "/")
|
||||
namespace := s.ns
|
||||
functionName := ""
|
||||
remainingPath := ""
|
||||
|
||||
if len(parts) == 1 {
|
||||
functionName = strings.TrimSpace(parts[0])
|
||||
} else {
|
||||
namespace = strings.TrimSpace(parts[0])
|
||||
functionName = strings.TrimSpace(parts[1])
|
||||
if len(parts) > 2 {
|
||||
remainingPath = "/" + strings.Join(parts[2:], "/")
|
||||
}
|
||||
}
|
||||
|
||||
if namespace == "" || functionName == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "namespace and function name are required")
|
||||
return
|
||||
}
|
||||
|
||||
s.invokeInternalFunction(w, r, namespace, functionName, remainingPath)
|
||||
}
|
||||
|
||||
func (s *Server) invokeInternalFunction(w http.ResponseWriter, r *http.Request, namespace, functionName, extraPath string) {
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("read request body: %v", err))
|
||||
return
|
||||
}
|
||||
if len(bytes.TrimSpace(bodyBytes)) == 0 {
|
||||
bodyBytes = []byte("{}")
|
||||
}
|
||||
|
||||
lookupCtx, lookupCancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer lookupCancel()
|
||||
|
||||
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(namespace).Get(lookupCtx, functionName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", functionName))
|
||||
return
|
||||
}
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get function %q: %v", functionName, err))
|
||||
return
|
||||
}
|
||||
|
||||
invokeTimeout := s.resolveInvokeTimeout(fn)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
|
||||
defer cancel()
|
||||
|
||||
invokeURL := buildInternalInvokeURL(s.routerURL, namespace, functionName) + extraPath
|
||||
if r.URL.RawQuery != "" {
|
||||
invokeURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
var invokeBody io.Reader
|
||||
if shouldForwardRequestBody(r.Method) {
|
||||
invokeBody = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, r.Method, invokeURL, invokeBody)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build invoke request: %v", err))
|
||||
return
|
||||
}
|
||||
if shouldForwardRequestBody(r.Method) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
copyProxyRequestHeaders(req.Header, r.Header)
|
||||
if token := s.getRouterToken(); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := doRequestWithContextTimeout(s.http, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s", functionName, invokeTimeout))
|
||||
return
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s", functionName, invokeTimeout))
|
||||
return
|
||||
}
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", functionName, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
writeAnyJSON(w, http.StatusOK, map[string]any{
|
||||
"status": resp.StatusCode,
|
||||
"latency_ms": time.Since(start).Milliseconds(),
|
||||
"invoke_url": invokeURL,
|
||||
"response_raw": string(respBody),
|
||||
})
|
||||
}
|
||||
|
||||
// handleInvokeRoute даёт пользователю прямой HTTP gateway к своей функции по route.
|
||||
// Внешний контракт: /fn/<route> + Authorization: Bearer <user-token>.
|
||||
func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user