From 6731e8998bc134eb5f9fecbdc7da20094133b382 Mon Sep 17 00:00:00 2001 From: Naeel Date: Wed, 15 Apr 2026 07:33:00 +0300 Subject: [PATCH] feat: deliver fission console UI, k8s deploy, and tests --- console/Dockerfile | 12 + console/deploy/console.yaml | 114 +++++++++ console/go.mod | 11 + console/go.sum | 156 ++++++++++++ console/main.go | 147 +++++++++++- console/main_test.go | 143 +++++++++++ console/ui/index.html | 457 +++++++++++++++++++++++++++++++++++- 7 files changed, 1016 insertions(+), 24 deletions(-) create mode 100644 console/Dockerfile create mode 100644 console/deploy/console.yaml create mode 100644 console/go.sum create mode 100644 console/main_test.go diff --git a/console/Dockerfile b/console/Dockerfile new file mode 100644 index 0000000..a8491c9 --- /dev/null +++ b/console/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.26-alpine AS builder +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o fission-console . + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates +COPY --from=builder /build/fission-console /fission-console +EXPOSE 8090 +ENTRYPOINT ["/fission-console"] diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml new file mode 100644 index 0000000..92fa11e --- /dev/null +++ b/console/deploy/console.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fission-console + namespace: fission +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: fission-console +rules: + - apiGroups: ["fission.io"] + resources: ["environments", "packages", "functions", "httptriggers", "timetriggers"] + verbs: ["get", "list", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: fission-console +subjects: + - kind: ServiceAccount + name: fission-console + namespace: fission +roleRef: + kind: ClusterRole + name: fission-console + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fission-console + namespace: fission + labels: + app: fission-console +spec: + replicas: 1 + selector: + matchLabels: + app: fission-console + template: + metadata: + labels: + app: fission-console + spec: + serviceAccountName: fission-console + containers: + - name: console + image: naeel/fission-console:v0.1.5 + ports: + - containerPort: 8090 + env: + - name: FISSION_NAMESPACE + value: "default" + - name: FISSION_ROUTER_URL + value: "http://router.fission.svc.cluster.local" + - name: PORT + value: "8090" + livenessProbe: + httpGet: + path: /health + port: 8090 + initialDelaySeconds: 5 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health + port: 8090 + initialDelaySeconds: 3 + periodSeconds: 10 + resources: + requests: + cpu: 20m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: fission-console + namespace: fission +spec: + selector: + app: fission-console + ports: + - port: 8090 + targetPort: 8090 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: fission-console + namespace: fission + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" +spec: + ingressClassName: nginx + rules: + - host: fission.kube5s.ru + http: + paths: + - path: /console + pathType: Prefix + backend: + service: + name: fission-console + port: + number: 8090 + tls: + - hosts: + - fission.kube5s.ru + secretName: fission-tls diff --git a/console/go.mod b/console/go.mod index 62c12dd..e531983 100644 --- a/console/go.mod +++ b/console/go.mod @@ -11,21 +11,32 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect diff --git a/console/go.sum b/console/go.sum new file mode 100644 index 0000000..778ac55 --- /dev/null +++ b/console/go.sum @@ -0,0 +1,156 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/console/main.go b/console/main.go index 48afef6..ee4bbea 100644 --- a/console/main.go +++ b/console/main.go @@ -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 +} diff --git a/console/main_test.go b/console/main_test.go new file mode 100644 index 0000000..36adfe2 --- /dev/null +++ b/console/main_test.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + dynamicfake "k8s.io/client-go/dynamic/fake" +) + +func newTestServer(objs ...runtime.Object) *server { + listKinds := map[schema.GroupVersionResource]string{ + environmentGVR: "EnvironmentList", + packageGVR: "PackageList", + functionGVR: "FunctionList", + httpTrigGVR: "HTTPTriggerList", + timeTrigGVR: "TimeTriggerList", + } + dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, objs...) + return &server{dyn: dyn, ns: "default", routerURL: "http://example.invalid", http: &http.Client{}} +} + +func TestNormalizeMethods(t *testing.T) { + got := normalizeMethods([]string{"get", " POST ", "get", ""}) + if len(got) != 2 || got[0] != "GET" || got[1] != "POST" { + t.Fatalf("unexpected methods: %#v", got) + } + + fallback := normalizeMethods([]string{"", " "}) + if len(fallback) != 1 || fallback[0] != "GET" { + t.Fatalf("expected default GET, got %#v", fallback) + } +} + +func TestCreateFunctionValidation(t *testing.T) { + s := newTestServer() + req := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(`{"name":""}`)) + rec := httptest.NewRecorder() + + s.handleCreateFunction(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String()) + } +} + +func TestCreateFunctionSuccessAndGetDetails(t *testing.T) { + env := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "Environment", + "metadata": map[string]any{ + "name": "python", + "namespace": "default", + }, + }} + s := newTestServer(env) + + body := `{"name":"t-fn","environment":"python","code":"def main(ctx):\n return {\"ok\": True}","entrypoint":"main.main","route":"/t-fn","methods":["get"]}` + req := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(body)) + rec := httptest.NewRecorder() + s.handleCreateFunction(rec, req) + if rec.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String()) + } + + ctx := context.Background() + pkg, err := s.dyn.Resource(packageGVR).Namespace("default").Get(ctx, "t-fn-pkg", metav1.GetOptions{}) + if err != nil { + t.Fatalf("package not created: %v", err) + } + literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal") + decoded, err := base64.StdEncoding.DecodeString(literal) + if err != nil { + t.Fatalf("decode literal: %v", err) + } + if !strings.Contains(string(decoded), "def main") { + t.Fatalf("unexpected package code: %q", string(decoded)) + } + + getReq := httptest.NewRequest(http.MethodGet, "/api/functions/t-fn", nil) + getRec := httptest.NewRecorder() + s.handleGetFunction(getRec, getReq, "t-fn") + 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) + } + if out["name"] != "t-fn" { + t.Fatalf("unexpected name: %#v", out["name"]) + } + if out["environment"] != "python" { + t.Fatalf("unexpected environment: %#v", out["environment"]) + } +} + +func TestUpdateFunctionCode(t *testing.T) { + env := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "fission.io/v1", + "kind": "Environment", + "metadata": map[string]any{"name": "python", "namespace": "default"}, + }} + s := newTestServer(env) + + createReq := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(`{"name":"upd-fn","environment":"python","code":"old","entrypoint":"main.main"}`)) + createRec := httptest.NewRecorder() + s.handleCreateFunction(createRec, createReq) + if createRec.Code != http.StatusCreated { + t.Fatalf("create failed: %d %s", createRec.Code, createRec.Body.String()) + } + + updReq := httptest.NewRequest(http.MethodPut, "/api/functions/upd-fn/code", bytes.NewBufferString(`{"code":"new-code"}`)) + updRec := httptest.NewRecorder() + s.handleUpdateFunctionCode(updRec, updReq, "upd-fn") + if updRec.Code != http.StatusOK { + t.Fatalf("update failed: %d %s", updRec.Code, updRec.Body.String()) + } + + ctx := context.Background() + pkg, err := s.dyn.Resource(packageGVR).Namespace("default").Get(ctx, "upd-fn-pkg", metav1.GetOptions{}) + if err != nil { + t.Fatalf("get package: %v", err) + } + literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal") + decoded, err := base64.StdEncoding.DecodeString(literal) + if err != nil { + t.Fatalf("decode literal: %v", err) + } + if string(decoded) != "new-code" { + t.Fatalf("expected new-code, got %q", string(decoded)) + } +} diff --git a/console/ui/index.html b/console/ui/index.html index 41c95d9..36e2283 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -47,6 +47,20 @@ padding: 8px 12px; cursor: pointer; font-size: 13px; + line-height: 1.2; + } + + .btn:disabled { + opacity: .6; + cursor: not-allowed; + } + + .btn.danger { + background: #b43232; + } + + .btn.ghost { + background: #0c2b49; } .wrap { @@ -90,6 +104,14 @@ margin-top: 12px; } + .toolbar { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: center; + margin-bottom: 10px; + } + table { width: 100%; border-collapse: collapse; @@ -108,12 +130,124 @@ font-size: 12px; margin-top: 8px; } + + .mono { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + } + + .status { + margin-top: 10px; + padding: 9px 10px; + border-radius: 6px; + font-size: 13px; + background: #0c2b49; + border: 1px solid #1d486d; + display: none; + } + + .status.ok { + background: #103625; + border-color: #236843; + } + + .status.err { + background: #411d1d; + border-color: #7a2f2f; + } + + .chip { + display: inline-block; + padding: 2px 7px; + border-radius: 999px; + border: 1px solid #1d486d; + background: #0c2b49; + font-size: 11px; + margin-right: 4px; + } + + .modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,.45); + display: none; + align-items: center; + justify-content: center; + padding: 16px; + } + + .modal.open { display: flex; } + + .panel { + width: min(820px, 100%); + max-height: 90vh; + overflow: auto; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 16px; + } + + .panel h3 { + margin-bottom: 10px; + font-size: 16px; + } + + .row { + display: flex; + gap: 10px; + margin-bottom: 10px; + flex-wrap: wrap; + } + + .field { + flex: 1; + min-width: 180px; + } + + label { + display: block; + margin-bottom: 4px; + font-size: 12px; + color: var(--text-secondary); + } + + input, select, textarea { + width: 100%; + border: 1px solid #1d486d; + background: #03192c; + color: var(--text-primary); + border-radius: 6px; + padding: 8px 10px; + font-size: 13px; + } + + textarea { + min-height: 220px; + resize: vertical; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + } + + .actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 8px; + flex-wrap: wrap; + } + + .nowrap { + white-space: nowrap; + }
@@ -126,40 +260,319 @@
-
Functions (MVP list)
+
+
Functions
+
Actions: view, edit code, invoke, delete
+
- +
NameEnvironmentPackage
NameEnvironmentPackageRouteMethodsActions
-
Initial MVP. Next step: create/edit/invoke/delete.
+
+
Namespace: default. CRUD происходит напрямую через CRD Fission.
+
+ + + + + + +