feat: deliver fission console UI, k8s deploy, and tests
This commit is contained in:
+135
-12
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user