feat: deliver fission console UI, k8s deploy, and tests

This commit is contained in:
Naeel
2026-04-15 07:33:00 +03:00
parent 7e2278371a
commit 6731e8998b
7 changed files with 1016 additions and 24 deletions
+135 -12
View File
@@ -14,6 +14,7 @@ import (
"time"
"fission-console/ui"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -79,6 +80,10 @@ func main() {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("ok\n"))
})
mux.HandleFunc("/console/health", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("ok\n"))
})
uiHandler := ui.Handler()
mux.Handle("/console", uiHandler)
@@ -91,6 +96,13 @@ func main() {
mux.HandleFunc("/api/httptriggers", s.handleList(httpTrigGVR))
mux.HandleFunc("/api/timetriggers", s.handleList(timeTrigGVR))
mux.HandleFunc("/console/api/environments", s.handleList(environmentGVR))
mux.HandleFunc("/console/api/packages", s.handleList(packageGVR))
mux.HandleFunc("/console/api/functions", s.handleFunctionsRoot)
mux.HandleFunc("/console/api/functions/", s.handleFunctionsAction)
mux.HandleFunc("/console/api/httptriggers", s.handleList(httpTrigGVR))
mux.HandleFunc("/console/api/timetriggers", s.handleList(timeTrigGVR))
httpServer := &http.Server{
Addr: ":" + port,
Handler: withCORS(logRequests(mux)),
@@ -114,6 +126,9 @@ func (s *server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
func (s *server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/functions/")
if path == r.URL.Path {
path = strings.TrimPrefix(r.URL.Path, "/console/api/functions/")
}
path = strings.Trim(path, "/")
if path == "" {
http.NotFound(w, r)
@@ -175,9 +190,10 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
if req.Route == "" {
req.Route = "/" + req.Name
}
if len(req.Methods) == 0 {
req.Methods = []string{"GET"}
if !strings.HasPrefix(req.Route, "/") {
req.Route = "/" + req.Route
}
req.Methods = normalizeMethods(req.Methods)
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
@@ -189,6 +205,10 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
pkgName := req.Name + "-pkg"
triggerName := req.Name + "-route"
methodValues := make([]any, 0, len(req.Methods))
for _, method := range req.Methods {
methodValues = append(methodValues, method)
}
literal := base64.StdEncoding.EncodeToString([]byte(req.Code))
pkg := &unstructured.Unstructured{Object: map[string]any{
@@ -233,7 +253,7 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
"StrategyType": "execution",
},
"package": map[string]any{
"packageref": map[string]any{"name": pkgName, "namespace": s.ns},
"packageref": map[string]any{"name": pkgName, "namespace": s.ns},
"functionName": req.Entrypoint,
},
},
@@ -254,7 +274,7 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
},
"spec": map[string]any{
"relativeurl": req.Route,
"methods": req.Methods,
"methods": methodValues,
"createingress": true,
"functionref": map[string]any{
"type": "name",
@@ -292,7 +312,50 @@ func (s *server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
return
}
writeAnyJSON(w, http.StatusOK, fn.Object)
packageName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
environment, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
entrypoint, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "functionName")
code := ""
if packageName != "" {
pkg, pkgErr := s.dyn.Resource(packageGVR).Namespace(s.ns).Get(ctx, packageName, metav1.GetOptions{})
if pkgErr == nil {
literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal")
if literal != "" {
decoded, decErr := base64.StdEncoding.DecodeString(literal)
if decErr == nil {
code = string(decoded)
}
}
}
}
route := ""
methods := []string{}
triggers, trigErr := s.dyn.Resource(httpTrigGVR).Namespace(s.ns).List(ctx, metav1.ListOptions{})
if trigErr == nil {
for _, trig := range triggers.Items {
refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name")
if refName != name {
continue
}
route, _, _ = unstructured.NestedString(trig.Object, "spec", "relativeurl")
methods, _, _ = unstructured.NestedStringSlice(trig.Object, "spec", "methods")
break
}
}
writeAnyJSON(w, http.StatusOK, map[string]any{
"name": name,
"namespace": s.ns,
"environment": environment,
"package": packageName,
"entrypoint": entrypoint,
"code": code,
"route": route,
"methods": methods,
"raw": fn.Object,
})
}
func (s *server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request, name string) {
@@ -364,13 +427,53 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
defer cancel()
invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name)
invokeMethod := http.MethodPost
triggers, err := s.dyn.Resource(httpTrigGVR).Namespace(s.ns).List(ctx, metav1.ListOptions{})
if err == nil {
for _, trig := range triggers.Items {
refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name")
if refName != name {
continue
}
route, _, _ := unstructured.NestedString(trig.Object, "spec", "relativeurl")
methods, _, _ := unstructured.NestedStringSlice(trig.Object, "spec", "methods")
hasPost := false
hasGet := false
for _, method := range methods {
m := strings.ToUpper(strings.TrimSpace(method))
if m == http.MethodPost {
hasPost = true
}
if m == http.MethodGet {
hasGet = true
}
}
if route != "" {
if !strings.HasPrefix(route, "/") {
route = "/" + route
}
invokeURL = s.routerURL + route
if !hasPost && hasGet {
invokeMethod = http.MethodGet
}
break
}
}
}
start := time.Now()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, invokeURL, bytes.NewReader(bodyBytes))
var invokeBody io.Reader
if invokeMethod == http.MethodPost {
invokeBody = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequestWithContext(ctx, invokeMethod, invokeURL, invokeBody)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build invoke request: %v", err))
return
}
req.Header.Set("Content-Type", "application/json")
if invokeMethod == http.MethodPost {
req.Header.Set("Content-Type", "application/json")
}
resp, err := s.http.Do(req)
if err != nil {
@@ -381,9 +484,9 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
respBody, _ := io.ReadAll(resp.Body)
writeAnyJSON(w, http.StatusOK, map[string]any{
"status": resp.StatusCode,
"latency_ms": time.Since(start).Milliseconds(),
"invoke_url": invokeURL,
"status": resp.StatusCode,
"latency_ms": time.Since(start).Milliseconds(),
"invoke_url": invokeURL,
"response_raw": string(respBody),
})
}
@@ -409,12 +512,12 @@ func (s *server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, na
}
if err := s.dyn.Resource(functionGVR).Namespace(s.ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete function %q: %v", name, err))
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete function %q: %v", name, err))
return
}
if pkgName != "" {
if err := s.dyn.Resource(packageGVR).Namespace(s.ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
if err := s.dyn.Resource(packageGVR).Namespace(s.ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err))
return
}
@@ -506,3 +609,23 @@ func envDefault(key, fallback string) string {
}
return fallback
}
func normalizeMethods(in []string) []string {
if len(in) == 0 {
return []string{"GET"}
}
out := make([]string, 0, len(in))
seen := map[string]bool{}
for _, method := range in {
m := strings.ToUpper(strings.TrimSpace(method))
if m == "" || seen[m] {
continue
}
seen[m] = true
out = append(out, m)
}
if len(out) == 0 {
return []string{"GET"}
}
return out
}