diff --git a/.github/workflows/check-actions.yaml b/.github/workflows/check-actions.yaml index 3b588d2a..3fb1da97 100644 --- a/.github/workflows/check-actions.yaml +++ b/.github/workflows/check-actions.yaml @@ -12,16 +12,6 @@ jobs: required: runs-on: ubuntu-latest steps: - - name: Free disk space - uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Ensure SHA pinned actions diff --git a/.github/workflows/codegen.yaml b/.github/workflows/codegen.yaml index 23f2eca1..92633d26 100644 --- a/.github/workflows/codegen.yaml +++ b/.github/workflows/codegen.yaml @@ -16,16 +16,6 @@ jobs: required: runs-on: ubuntu-latest steps: - - name: Free disk space - uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a7308831..a3bbf66f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -16,16 +16,6 @@ jobs: required: runs-on: ubuntu-latest steps: - - name: Free disk space - uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a1ca863b..89108f0c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,16 +16,6 @@ jobs: required: runs-on: ubuntu-latest steps: - - name: Free disk space - uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 diff --git a/.gitignore b/.gitignore index 274f3a25..efda18f6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /kyverno-json website/site website/playground/assets/main.wasm +pkg/server/ui/dist/assets/main.wasm diff --git a/Makefile b/Makefile index 27cacdbd..6ff5d715 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,7 @@ vet: ## Run go vet @echo Go vet... >&2 @go vet ./... -$(CLI_BIN): fmt vet build-wasm codegen-crds codegen-deepcopy codegen-register codegen-client +$(CLI_BIN): fmt vet build-wasm codegen-crds codegen-deepcopy codegen-register codegen-client codegen-playground @echo Build cli binary... >&2 @CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build -o ./$(CLI_BIN) -ldflags=$(LD_FLAGS) ./$(CLI_DIR) @@ -270,6 +270,11 @@ codegen-schemas-json: codegen-schemas-openapi ## Generate json schemas .PHONY: codegen-schemas codegen-schemas: codegen-schemas-openapi codegen-schemas-json ## Generate openapi and json schemas +.PHONY: codegen-playground +codegen-playground: build-wasm ## Generate playground + @echo Generate playground... >&2 + @cp -r ./website/playground/* ./pkg/server/ui/dist + .PHONY: codegen-helm-crds codegen-helm-crds: codegen-crds ## Generate helm CRDs @echo Generate helm crds... >&2 @@ -292,7 +297,7 @@ codegen-helm-docs: ## Generate helm docs @docker run -v ${PWD}/charts:/work -w /work jnorwood/helm-docs:v1.11.0 -s file .PHONY: codegen -codegen: codegen-crds codegen-deepcopy codegen-register codegen-client codegen-docs codegen-mkdocs codegen-schemas codegen-helm-docs ## Rebuild all generated code and docs +codegen: codegen-crds codegen-deepcopy codegen-register codegen-client codegen-docs codegen-mkdocs codegen-schemas codegen-playground codegen-helm-crds codegen-helm-docs ## Rebuild all generated code and docs .PHONY: verify-codegen verify-codegen: codegen ## Verify all generated code and docs are up to date diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go index e2c93dac..270405fd 100644 --- a/cmd/wasm/main.go +++ b/cmd/wasm/main.go @@ -1,17 +1,3 @@ -// Copyright 2023 Undistro Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - //go:build js && wasm package main @@ -20,35 +6,28 @@ import ( "context" "os/signal" "syscall" - "time" - server "github.com/kyverno/kyverno-json/pkg/server/wasm" + "github.com/gin-gonic/gin" + "github.com/kyverno/kyverno-json/pkg/server" + "github.com/kyverno/kyverno-json/pkg/server/playground" ) func main() { // initialise gin framework - // gin.SetMode(c.ginFlags.mode) - // tonic.SetBindHook(tonic.DefaultBindingHookMaxBodyBytes(int64(c.ginFlags.maxBodySize))) + gin.SetMode(gin.DebugMode) // create server - server, err := server.New(true, true) + router, err := server.New(true, true) if err != nil { panic(err) } // register playground routes - if err := server.AddPlaygroundRoutes(); err != nil { + if err := playground.AddRoutes(router.Group(server.PlaygroundPrefix)); err != nil { panic(err) } // run server ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() - shutdown := server.Run(ctx) + server.RunWasm(ctx, router) <-ctx.Done() stop() - if shutdown != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := shutdown(ctx); err != nil { - panic(err) - } - } } diff --git a/docs/user/commands/kyverno-json.md b/docs/user/commands/kyverno-json.md index f6260017..26e143aa 100644 --- a/docs/user/commands/kyverno-json.md +++ b/docs/user/commands/kyverno-json.md @@ -22,6 +22,7 @@ kyverno-json [flags] * [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell * [kyverno-json docs](kyverno-json_docs.md) - Generates reference documentation. * [kyverno-json jp](kyverno-json_jp.md) - Provides a command-line interface to JMESPath, enhanced with custom functions. +* [kyverno-json playground](kyverno-json_playground.md) - playground * [kyverno-json scan](kyverno-json_scan.md) - scan * [kyverno-json serve](kyverno-json_serve.md) - serve * [kyverno-json version](kyverno-json_version.md) - Prints the version informations. diff --git a/docs/user/commands/kyverno-json_playground.md b/docs/user/commands/kyverno-json_playground.md new file mode 100644 index 00000000..11519777 --- /dev/null +++ b/docs/user/commands/kyverno-json_playground.md @@ -0,0 +1,28 @@ +## kyverno-json playground + +playground + +### Synopsis + +Serve playground + +``` +kyverno-json playground [flags] +``` + +### Options + +``` + --gin-cors enable gin cors (default true) + --gin-log enable gin logger (default true) + --gin-max-body-size int gin max body size (default 2097152) + --gin-mode string gin run mode (default "release") + -h, --help help for playground + --server-host string server host (default "0.0.0.0") + --server-port int server port (default 8080) +``` + +### SEE ALSO + +* [kyverno-json](kyverno-json.md) - kyverno-json is a CLI tool to apply policies to json resources. + diff --git a/pkg/commands/playground/command.go b/pkg/commands/playground/command.go new file mode 100644 index 00000000..6f1f28a1 --- /dev/null +++ b/pkg/commands/playground/command.go @@ -0,0 +1,27 @@ +package playground + +import ( + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" +) + +func Command(parents ...string) *cobra.Command { + var command options + cmd := &cobra.Command{ + Use: "playground", + Short: "playground", + Long: "Serve playground", + Args: cobra.NoArgs, + SilenceUsage: true, + RunE: command.Run, + } + // server flags + cmd.Flags().StringVar(&command.serverFlags.host, "server-host", "0.0.0.0", "server host") + cmd.Flags().IntVar(&command.serverFlags.port, "server-port", 8080, "server port") + // gin flags + cmd.Flags().StringVar(&command.ginFlags.mode, "gin-mode", gin.ReleaseMode, "gin run mode") + cmd.Flags().BoolVar(&command.ginFlags.log, "gin-log", true, "enable gin logger") + cmd.Flags().BoolVar(&command.ginFlags.cors, "gin-cors", true, "enable gin cors") + cmd.Flags().IntVar(&command.ginFlags.maxBodySize, "gin-max-body-size", 2*1024*1024, "gin max body size") + return cmd +} diff --git a/pkg/commands/playground/options.go b/pkg/commands/playground/options.go new file mode 100644 index 00000000..d337e970 --- /dev/null +++ b/pkg/commands/playground/options.go @@ -0,0 +1,60 @@ +package playground + +import ( + "context" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/kyverno/kyverno-json/pkg/server" + "github.com/kyverno/kyverno-json/pkg/server/ui" + "github.com/loopfz/gadgeto/tonic" + "github.com/spf13/cobra" +) + +type options struct { + serverFlags serverFlags + ginFlags ginFlags +} + +type serverFlags struct { + host string + port int +} + +type ginFlags struct { + mode string + log bool + cors bool + maxBodySize int +} + +func (c *options) Run(_ *cobra.Command, _ []string) error { + // initialise gin framework + gin.SetMode(c.ginFlags.mode) + tonic.SetBindHook(tonic.DefaultBindingHookMaxBodyBytes(int64(c.ginFlags.maxBodySize))) + // create router + router, err := server.New(c.ginFlags.log, c.ginFlags.cors) + if err != nil { + return err + } + // register api routes + if err := ui.AddRoutes(router); err != nil { + return err + } + // run server + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + shutdown := server.Run(ctx, router, c.serverFlags.host, c.serverFlags.port) + <-ctx.Done() + stop() + if shutdown != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := shutdown(ctx); err != nil { + return err + } + } + return nil +} diff --git a/pkg/commands/root.go b/pkg/commands/root.go index 454e5fe3..755c0b85 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -4,6 +4,7 @@ import ( "github.com/kyverno/kyverno-json/pkg/command" "github.com/kyverno/kyverno-json/pkg/commands/docs" "github.com/kyverno/kyverno-json/pkg/commands/jp" + "github.com/kyverno/kyverno-json/pkg/commands/playground" "github.com/kyverno/kyverno-json/pkg/commands/scan" "github.com/kyverno/kyverno-json/pkg/commands/serve" "github.com/kyverno/kyverno-json/pkg/commands/version" @@ -28,6 +29,7 @@ func RootCommand() *cobra.Command { cmd.AddCommand( docs.Command("kyverno-json"), jp.Command("kyverno-json"), + playground.Command(), scan.Command(), serve.Command("kyverno-json"), version.Command("kyverno-json"), diff --git a/pkg/commands/root_test.go b/pkg/commands/root_test.go index ac23a7a6..96e4345e 100644 --- a/pkg/commands/root_test.go +++ b/pkg/commands/root_test.go @@ -12,7 +12,7 @@ import ( func TestRootCommand(t *testing.T) { cmd := RootCommand() assert.NotNil(t, cmd) - assert.Len(t, cmd.Commands(), 5) + assert.Len(t, cmd.Commands(), 6) err := cmd.Execute() assert.NoError(t, err) } diff --git a/pkg/commands/serve/options.go b/pkg/commands/serve/options.go index f3d07b7c..89b99007 100644 --- a/pkg/commands/serve/options.go +++ b/pkg/commands/serve/options.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/kyverno/kyverno-json/pkg/client/clientset/versioned" "github.com/kyverno/kyverno-json/pkg/server" - "github.com/kyverno/kyverno-json/pkg/server/api" + "github.com/kyverno/kyverno-json/pkg/server/scan" restutils "github.com/kyverno/kyverno-json/pkg/utils/rest" "github.com/loopfz/gadgeto/tonic" "github.com/spf13/cobra" @@ -42,8 +42,8 @@ func (c *options) Run(_ *cobra.Command, _ []string) error { // initialise gin framework gin.SetMode(c.ginFlags.mode) tonic.SetBindHook(tonic.DefaultBindingHookMaxBodyBytes(int64(c.ginFlags.maxBodySize))) - // create server - server, err := server.New(c.ginFlags.log, c.ginFlags.cors) + // create router + router, err := server.New(c.ginFlags.log, c.ginFlags.cors) if err != nil { return err } @@ -55,19 +55,17 @@ func (c *options) Run(_ *cobra.Command, _ []string) error { if err != nil { return err } - config := api.Configuration{ - PolicyProvider: &provider{ - client: client, - }, + provider := &provider{ + client: client, } // register api routes - if err := server.AddApiRoutes(config); err != nil { + if err := scan.AddRoutes(router.Group(server.ApiPrefix), provider); err != nil { return err } // run server ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() - shutdown := server.Run(ctx, c.serverFlags.host, c.serverFlags.port) + shutdown := server.Run(ctx, router, c.serverFlags.host, c.serverFlags.port) <-ctx.Done() stop() if shutdown != nil { diff --git a/pkg/server/api/config.go b/pkg/server/api/config.go deleted file mode 100644 index c2215dfd..00000000 --- a/pkg/server/api/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import ( - "github.com/kyverno/kyverno-json/pkg/server/api/scan" -) - -type Configuration struct { - PolicyProvider scan.PolicyProvider -} diff --git a/pkg/server/api/routes.go b/pkg/server/api/routes.go deleted file mode 100644 index 5d7a793f..00000000 --- a/pkg/server/api/routes.go +++ /dev/null @@ -1,13 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" - "github.com/kyverno/kyverno-json/pkg/server/api/scan" -) - -func AddRoutes(group *gin.RouterGroup, config Configuration) error { - if err := scan.AddRoutes(group, config.PolicyProvider); err != nil { - return err - } - return nil -} diff --git a/pkg/server/linux.go b/pkg/server/linux.go new file mode 100644 index 00000000..67d615ce --- /dev/null +++ b/pkg/server/linux.go @@ -0,0 +1,25 @@ +//go:build !js && !wasm + +package server + +import ( + "context" + "fmt" + "net/http" + "time" +) + +func Run(_ context.Context, s Server, host string, port int) Shutdown { + address := fmt.Sprintf("%v:%v", host, port) + srv := &http.Server{ + Addr: address, + Handler: s.Handler(), + ReadHeaderTimeout: 3 * time.Second, + } + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + return srv.Shutdown +} diff --git a/pkg/server/playground/scan/handler.go b/pkg/server/playground/handler.go similarity index 98% rename from pkg/server/playground/scan/handler.go rename to pkg/server/playground/handler.go index 9eee219c..d1c4581d 100644 --- a/pkg/server/playground/scan/handler.go +++ b/pkg/server/playground/handler.go @@ -1,4 +1,4 @@ -package scan +package playground import ( "context" diff --git a/pkg/server/playground/scan/request.go b/pkg/server/playground/request.go similarity index 88% rename from pkg/server/playground/scan/request.go rename to pkg/server/playground/request.go index df49a2e2..cfdd0e1e 100644 --- a/pkg/server/playground/scan/request.go +++ b/pkg/server/playground/request.go @@ -1,4 +1,4 @@ -package scan +package playground type Request struct { Payload string `json:"payload"` diff --git a/pkg/server/playground/scan/response.go b/pkg/server/playground/response.go similarity index 97% rename from pkg/server/playground/scan/response.go rename to pkg/server/playground/response.go index 0e9cde0d..6ab95592 100644 --- a/pkg/server/playground/scan/response.go +++ b/pkg/server/playground/response.go @@ -1,4 +1,4 @@ -package scan +package playground import ( "github.com/kyverno/kyverno-json/pkg/apis/v1alpha1" diff --git a/pkg/server/playground/routes.go b/pkg/server/playground/routes.go index d8ef8911..ed904524 100644 --- a/pkg/server/playground/routes.go +++ b/pkg/server/playground/routes.go @@ -2,12 +2,13 @@ package playground import ( "github.com/gin-gonic/gin" - "github.com/kyverno/kyverno-json/pkg/server/playground/scan" ) func AddRoutes(group *gin.RouterGroup) error { - if err := scan.AddRoutes(group); err != nil { + handler, err := newHandler() + if err != nil { return err } + group.POST("/scan", handler) return nil } diff --git a/pkg/server/playground/scan/routes.go b/pkg/server/playground/scan/routes.go deleted file mode 100644 index ae45a5f8..00000000 --- a/pkg/server/playground/scan/routes.go +++ /dev/null @@ -1,14 +0,0 @@ -package scan - -import ( - "github.com/gin-gonic/gin" -) - -func AddRoutes(group *gin.RouterGroup) error { - handler, err := newHandler() - if err != nil { - return err - } - group.POST("/scan", handler) - return nil -} diff --git a/pkg/server/api/scan/config.go b/pkg/server/scan/config.go similarity index 100% rename from pkg/server/api/scan/config.go rename to pkg/server/scan/config.go diff --git a/pkg/server/api/scan/handler.go b/pkg/server/scan/handler.go similarity index 100% rename from pkg/server/api/scan/handler.go rename to pkg/server/scan/handler.go diff --git a/pkg/server/api/scan/request.go b/pkg/server/scan/request.go similarity index 100% rename from pkg/server/api/scan/request.go rename to pkg/server/scan/request.go diff --git a/pkg/server/api/scan/response.go b/pkg/server/scan/response.go similarity index 100% rename from pkg/server/api/scan/response.go rename to pkg/server/scan/response.go diff --git a/pkg/server/api/scan/routes.go b/pkg/server/scan/routes.go similarity index 100% rename from pkg/server/api/scan/routes.go rename to pkg/server/scan/routes.go diff --git a/pkg/server/server.go b/pkg/server/server.go index fd8f3350..80e0d400 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,32 +2,19 @@ package server import ( "context" - "fmt" - "net/http" - "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/kyverno/kyverno-json/pkg/server/api" - "github.com/kyverno/kyverno-json/pkg/server/playground" ) const ( - apiPrefix = "/api" - playgroundPrefix = "/playground" + ApiPrefix = "/api" + PlaygroundPrefix = "/playground" ) type Shutdown = func(context.Context) error -type Server interface { - AddApiRoutes(api.Configuration) error - AddPlaygroundRoutes() error - Run(context.Context, string, int) Shutdown -} - -type server struct { - *gin.Engine -} +type Server = *gin.Engine func New(enableLogger bool, enableCors bool) (Server, error) { router := gin.New() @@ -43,28 +30,5 @@ func New(enableLogger bool, enableCors bool) (Server, error) { ExposeHeaders: []string{"Content-Length"}, })) } - return server{router}, nil -} - -func (s server) AddApiRoutes(config api.Configuration) error { - return api.AddRoutes(s.Group(apiPrefix), config) -} - -func (s server) AddPlaygroundRoutes() error { - return playground.AddRoutes(s.Group(playgroundPrefix)) -} - -func (s server) Run(_ context.Context, host string, port int) Shutdown { - address := fmt.Sprintf("%v:%v", host, port) - srv := &http.Server{ - Addr: address, - Handler: s.Engine.Handler(), - ReadHeaderTimeout: 3 * time.Second, - } - go func() { - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - panic(err) - } - }() - return srv.Shutdown + return router, nil } diff --git a/pkg/server/ui/dist/assets/css/styles.css b/pkg/server/ui/dist/assets/css/styles.css new file mode 100644 index 00000000..a09562e9 --- /dev/null +++ b/pkg/server/ui/dist/assets/css/styles.css @@ -0,0 +1,391 @@ + body { + font-family: 'Inter', sans-serif; + margin: 0; + padding: 0; + background-color: #FCFCFC; +} + +main { + padding: 24px; +} + +a { + color: #2244BB; + text-decoration: underline; +} + +.kyverno-logo { + height: 24px; +} + +.navbar { + background-color: white; + width: 100%; + height: 72px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid rgba(0, 0, 0, 0.04); +} + +.navbar .title { + color: #243942; + font-weight: 500; + font-size: 1.125rem; +} + +.navbar .divider { + width: 1px; + height: 1rem; + background: rgba(0, 0, 0, 0.12); +} + +.navbar .logo { + display: flex; + align-items: center; + padding: 0 1rem; + padding: 1.5rem; + text-decoration: none; +} + +.navbar .logo>*+* { + margin-left: 1rem; +} + +.navbar span { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-size: 16px; + letter-spacing: 0.01em; + color: rgba(0, 0, 0, 0.5); +} + +.nav-links { + display: flex; + flex-direction: row; + align-items: center; + justify-content: right; + gap: 16px; +} + +.nav-links > button { + height: 40px; + white-space: nowrap; + display:flex; + align-items: center; + gap: 8px; + padding: 8px 24px; +} + +.nav-divider { + content: ''; + width: 1px; + height: 24px; + border-radius: 2px; + background: rgba(0, 0, 0, 0.16); +} + +.share-url__container { + display: none; + height: 40px; + padding-left: 8px; + justify-content: flex-end; + align-items: center; + gap: 8px; + border-radius: 4px; + border: 1px solid #E6E6E6; + background: #FFF; + position: relative; +} + +.share-url__input { + display: flex; + width: 220px; + flex-direction: column; + color: rgba(0, 0, 0, 0.60); + font-size: 12px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.12px; + border:none; +} + +.share-url__tooltip { + color: #3EAA63; + font-size: 12px; + font-weight: 500; + line-height: 120%; + letter-spacing: 0.12px; + padding: 4px 8px; + position: absolute; + border-radius: 4px; + background: #E6EBE8; + right: 0; + bottom: -30px; + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +.share-url__input:focus { + outline: none; +} + +.share-url__copy { + display: flex; + height: 100%; + padding: 8px 12px; + justify-content: center; + align-items: center; + gap: 10px; + background: #F5F5F5; + border:none; + cursor: pointer; +} + +.nav-link { + display: flex; + justify-content: center; + align-items: center; + padding: 24px; + gap: 8px; + + text-decoration: none; + color: #2244BB; +} + +/* Containers */ +.editor-container { + display: flex; + flex-direction: row; + /* 2 * 24px = 48px for padding, 73px for navbar */ + height: calc(100vh - (2 * 24px) - 73px); + gap: 24px; +} + +.editor-container>div { + flex: 1; +} + +.output-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +.output-container>* { + flex: 1; +} + +.editor { + display: flex; + flex-direction: column; + border-radius: 8px; + border: 1px solid #E3E3E3; + overflow: visible; +} + +.editor__header { + padding: 1rem; + border-bottom: 1px solid #E3E3E3; + display: flex; + justify-content: space-between; + align-items: center; +} + +.editor__header .title { + color: #34454C; + font-weight: 500; + font-size: 18px; + line-height: 40px; +} + +.editor__header .description { + color: #5D7B89; + font-weight: normal; + font-size: 14px; +} + +.editor>.editor__input { + height: 100%; + font-size: 14px; +} + +.editor.editor--output { + background: #1D2E35; + color: white; +} + +.editor.editor--output .editor__header { + border-color: #2D4753; +} + +.editor>.editor__output { + background: #1D2E35; + resize: none; + height: 100%; + padding: 12px; + font-size: 14px; +} + +.editor>.editor__output::placeholder { + color: #E1E7EA; +} + + +/* Buttons and Inputs */ + +.button { + padding: 4px 24px; + border-radius: 4px; + background: #2244BB; + color: white; + transition: all 0.2s ease-in-out; + cursor: pointer; + border: none; + font-weight: 500; + font-size: 14px; + line-height: 24px; + height: 32px; +} + +.button:hover { + background: #4466BB; +} + +.button:disabled { + background: #8B8692; +} + + +/* Footer */ + +footer { + display: flex; + justify-content: space-between; + color: rgba(0, 0, 0, 0.5); + margin: 0 24px; + font-size: 14px; +} + +footer .version { + padding: 4px 8px; + background: #EBEBEB; + border-radius: 4px; +} + +.version a { + text-decoration: none; + color: inherit; +} + +footer .langdef { + padding: 4px 4px; + border-radius: 4px; + margin-left: auto; +} + +/* Ace Custom */ + +.ace-clouds .ace_marker-layer .ace_active-line { + background: rgba(0, 0, 0, 0.07); +} + +.ace-clouds .ace_gutter-active-line { + background-color: #dcdcdc; +} + +.ace-clouds .ace_comment { + color: #848484 +} + +.ace-clouds .ace_string, +.ace-clouds .ace_keyword, +.ace-clouds .ace_meta.ace_tag { + color: #59328B; +} + + +.ace-clouds .ace_line, +.ace-clouds .ace_constant.ace_numeric, +.ace-clouds .ace_constant.ace_boolean { + color: #0E394C +} + +.tippy-tooltip { + background-color: white; + color: #3E525B; + padding: 0; +} + +.tippy-backdrop { + background-color: white; +} + +.tippy-content { + display: flex; + flex-direction: column; + border: 1px solid #E3E3E3; + overflow: hidden; + border-radius: 0.25rem; +} + +.example-item { + min-width: 280px; + background-color: white; + padding: 0.5rem 1rem; + border: none; + text-align: left; + line-height: 24px; + font-size: 14px; + color: #3E525B; + transition: all 0.2s; + cursor: pointer; + z-index: 1000; +} + +.example-item:hover { + background-color: rgba(132, 71, 209, 0.08); +} + +.examples__button { + padding: 8px 16px; + background-color: #FAFAFA; + border: 1px solid #E3E3E3; + border-radius: 4px; + color: #3E525B; + display: flex; + align-items: center; + gap: 8px; +} + +.examples__button > #example-name { + line-height: 24px; +} + +.nice-select > span.current { + display: block; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; +} + +.nice-select .list { + max-height: 50vh; + overflow-y: scroll; + padding-bottom: 0 !important; + margin: 0; +} + +.nice-select .list::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.nice-select .list::-webkit-scrollbar-track { + background: #F5F5F5; +} + +.nice-select .list::-webkit-scrollbar-thumb { + background: #E3E3E3; + border-radius: 4px; +} \ No newline at end of file diff --git a/pkg/server/ui/dist/assets/data.json b/pkg/server/ui/dist/assets/data.json new file mode 100644 index 00000000..89b1c4ba --- /dev/null +++ b/pkg/server/ui/dist/assets/data.json @@ -0,0 +1,3 @@ +{ + "examples": [] +} diff --git a/pkg/server/ui/dist/assets/img/Kyverno_320x320.png b/pkg/server/ui/dist/assets/img/Kyverno_320x320.png new file mode 100644 index 00000000..ca12f29c Binary files /dev/null and b/pkg/server/ui/dist/assets/img/Kyverno_320x320.png differ diff --git a/pkg/server/ui/dist/assets/img/favicon.ico b/pkg/server/ui/dist/assets/img/favicon.ico new file mode 100644 index 00000000..b86d2081 Binary files /dev/null and b/pkg/server/ui/dist/assets/img/favicon.ico differ diff --git a/pkg/server/ui/dist/assets/img/github.svg b/pkg/server/ui/dist/assets/img/github.svg new file mode 100644 index 00000000..c29fce57 --- /dev/null +++ b/pkg/server/ui/dist/assets/img/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/pkg/server/ui/dist/assets/js/editor.js b/pkg/server/ui/dist/assets/js/editor.js new file mode 100644 index 00000000..ed38a76f --- /dev/null +++ b/pkg/server/ui/dist/assets/js/editor.js @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Undistro Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const EDITOR_DEFAULTS = { + "policy-input": { + theme: "ace/theme/clouds", + mode: "ace/mode/yaml", + }, + "payload-input": { + theme: "ace/theme/clouds", + mode: "ace/mode/yaml", + }, +}; + +class AceEditor { + constructor(id) { + this.editor = ace.edit(id); + this.editor.setTheme(EDITOR_DEFAULTS[id].theme); + this.editor.setShowPrintMargin(false); + this.editor.getSession().setMode(EDITOR_DEFAULTS[id].mode); + this.editor.getSession().setUseWorker(false); + } + + setValue(value, cursorPosition = 0) { + this.editor.setValue(value, cursorPosition); + } + + getValue() { + return this.editor.getValue(); + } +} + +export { AceEditor }; diff --git a/pkg/server/ui/dist/assets/js/main.js b/pkg/server/ui/dist/assets/js/main.js new file mode 100644 index 00000000..14059c33 --- /dev/null +++ b/pkg/server/ui/dist/assets/js/main.js @@ -0,0 +1,137 @@ +import { AceEditor } from "./editor.js"; + +const selectInstance = NiceSelect.bind(document.getElementById("examples")); + +const policyEditor = new AceEditor("policy-input"); +const payloadEditor = new AceEditor("payload-input"); + +async function run() { + const policy = policyEditor.getValue(); + const payload = payloadEditor.getValue(); + const output = document.getElementById("output"); + output.value = "Evaluating..."; + try { + const reponse = await fetch("/api/playground/scan", { + method: "POST", + body: JSON.stringify({ + payload: payload, + policy: policy, + }) + }) + output.value = JSON.stringify(await reponse.json(), null, 2); + output.style.color = "white"; + } catch (error) { + output.value = error; + console.error("Error:", error); + output.style.color = "red"; + } +} + +function share() { + const payload = payloadEditor.getValue(); + const policy = policyEditor.getValue(); + + const obj = { + policy: policy, + payload: payload, + }; + + const str = JSON.stringify(obj); + var compressed_uint8array = pako.gzip(str); + var b64encoded_string = btoa( + String.fromCharCode.apply(null, compressed_uint8array) + ); + + const url = new URL(window.location.href); + url.searchParams.set("content", b64encoded_string); + window.history.pushState({}, "", url.toString()); + + document.querySelector(".share-url__container").style.display = "flex"; + document.querySelector(".share-url__input").value = url.toString(); +} + +var urlParams = new URLSearchParams(window.location.search); +if (urlParams.has("content")) { + const content = urlParams.get("content"); + try { + const decodedUint8Array = new Uint8Array( + atob(content) + .split("") + .map(function (char) { + return char.charCodeAt(0); + }) + ); + + const decompressedData = pako.ungzip(decodedUint8Array, { to: "string" }); + if (!decompressedData) { + throw new Error("Invalid content parameter"); + } + const obj = JSON.parse(decompressedData); + payloadEditor.setValue(obj.payload, -1); + policyEditor.setValue(obj.policy, -1); + } catch (error) { + console.error(error); + } +} + +function copy() { + const copyText = document.querySelector(".share-url__input"); + copyText.select(); + copyText.setSelectionRange(0, 99999); + navigator.clipboard.writeText(copyText.value); + window.getSelection().removeAllRanges(); + + const tooltip = document.querySelector(".share-url__tooltip"); + tooltip.style.opacity = 1; + setTimeout(() => { + tooltip.style.opacity = 0; + }, 3000); +} + +const runButton = document.getElementById("run"); +const shareButton = document.getElementById("share"); +const copyButton = document.getElementById("copy"); + +runButton.addEventListener("click", run); +shareButton.addEventListener("click", share); +copyButton.addEventListener("click", copy); +document.addEventListener("keydown", (event) => { + if ((event.ctrlKey || event.metaKey) && event.code === "Enter") { + run(); + } +}); + +fetch("../assets/data.json") + .then((response) => response.json()) + .then(({ examples }) => { + + // Load the examples into the select element + const examplesList = document.getElementById("examples"); + examples.forEach((example) => { + const option = document.createElement("option"); + option.value = example.name; + option.innerText = example.name; + + if (example.name === "default") { + if (!urlParams.has("content")) { + payloadEditor.setValue(example.payload, -1); + policyEditor.setValue(example.policy, -1); + } + } else { + examplesList.appendChild(option); + } + }); + + selectInstance.update(); + + examplesList.addEventListener("change", (event) => { + const example = examples.find( + (example) => example.name === event.target.value + ); + payloadEditor.setValue(example.payload, -1); + policyEditor.setValue(example.policy, -1); + }); + }) + .catch((err) => { + console.error(err); + }); \ No newline at end of file diff --git a/pkg/server/ui/dist/dist/nice-select2.css b/pkg/server/ui/dist/dist/nice-select2.css new file mode 100644 index 00000000..e15f19b3 --- /dev/null +++ b/pkg/server/ui/dist/dist/nice-select2.css @@ -0,0 +1 @@ +.nice-select{-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:#fff;border-radius:5px;border:solid 1px #e8e8e8;box-sizing:border-box;clear:both;cursor:pointer;display:block;float:left;font-family:inherit;font-size:14px;font-weight:normal;height:38px;line-height:36px;outline:none;padding-left:18px;padding-right:30px;position:relative;text-align:left !important;transition:all .2s ease-in-out;user-select:none;white-space:nowrap;width:auto}.nice-select:hover{border-color:#dbdbdb}.nice-select:active,.nice-select.open,.nice-select:focus{border-color:#999}.nice-select:after{border-bottom:2px solid #999;border-right:2px solid #999;content:"";display:block;height:5px;margin-top:-4px;pointer-events:none;position:absolute;right:12px;top:50%;transform-origin:66% 66%;transform:rotate(45deg);transition:all .15s ease-in-out;width:5px}.nice-select.open:after{transform:rotate(-135deg)}.nice-select.open .nice-select-dropdown{opacity:1;pointer-events:auto;transform:scale(1) translateY(0)}.nice-select.disabled{border-color:#ededed;color:#999;pointer-events:none}.nice-select.disabled:after{border-color:#ccc}.nice-select.wide{width:100%}.nice-select.wide .nice-select-dropdown{left:0 !important;right:0 !important}.nice-select.right{float:right}.nice-select.right .nice-select-dropdown{left:auto;right:0}.nice-select.small{font-size:12px;height:36px;line-height:34px}.nice-select.small:after{height:4px;width:4px}.nice-select.small .option{line-height:34px;min-height:34px}.nice-select .nice-select-dropdown{margin-top:4px;background-color:#fff;border-radius:5px;box-shadow:0 0 0 1px rgba(68,68,68,.11);pointer-events:none;position:absolute;top:100%;left:0;transform-origin:50% 0;transform:scale(0.75) translateY(19px);transition:all .2s cubic-bezier(0.5, 0, 0, 1.25),opacity .15s ease-out;z-index:9;opacity:0}.nice-select .list{border-radius:5px;box-sizing:border-box;overflow:hidden;padding:0;max-height:210px;overflow-y:auto}.nice-select .list:hover .option:not(:hover){background-color:rgba(0,0,0,0) !important}.nice-select .option{cursor:pointer;font-weight:400;line-height:40px;list-style:none;outline:none;padding-left:18px;padding-right:29px;text-align:left;transition:all .2s}.nice-select .option:hover,.nice-select .option.focus,.nice-select .option.selected.focus{background-color:#f6f6f6}.nice-select .option.selected{font-weight:bold}.nice-select .option.disabled{background-color:rgba(0,0,0,0);color:#999;cursor:default}.nice-select .optgroup{font-weight:bold}.no-csspointerevents .nice-select .nice-select-dropdown{display:none}.no-csspointerevents .nice-select.open .nice-select-dropdown{display:block}.nice-select .list::-webkit-scrollbar{width:0}.nice-select .has-multiple{white-space:inherit;height:auto;padding:7px 12px;min-height:36px;line-height:22px}.nice-select .has-multiple span.current{border:1px solid #ccc;background:#eee;padding:0 10px;border-radius:3px;display:inline-block;line-height:24px;font-size:14px;margin-bottom:3px;margin-right:3px}.nice-select .has-multiple .multiple-options{display:block;line-height:24px;padding:0}.nice-select .nice-select-search-box{box-sizing:border-box;width:100%;padding:5px;pointer-events:none;border-radius:5px 5px 0 0}.nice-select .nice-select-search{box-sizing:border-box;background-color:#fff;border:1px solid #e8e8e8;border-radius:3px;color:#444;display:inline-block;vertical-align:middle;padding:7px 12px;margin:0 10px 0 0;width:100%;min-height:36px;line-height:22px;height:auto;outline:0 !important;font-size:14px} diff --git a/pkg/server/ui/dist/dist/nice-select2.js b/pkg/server/ui/dist/dist/nice-select2.js new file mode 100644 index 00000000..1db8463e --- /dev/null +++ b/pkg/server/ui/dist/dist/nice-select2.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.NiceSelect=t():e.NiceSelect=t()}(self,(()=>(()=>{"use strict";var e={d:(t,i)=>{for(var s in i)e.o(i,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:i[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function i(e){var t=document.createEvent("MouseEvents");t.initEvent("click",!0,!1),e.dispatchEvent(t)}function s(e){var t=document.createEvent("HTMLEvents");t.initEvent("change",!0,!1),e.dispatchEvent(t)}function o(e){var t=document.createEvent("FocusEvent");t.initEvent("focusin",!0,!1),e.dispatchEvent(t)}function n(e){var t=document.createEvent("FocusEvent");t.initEvent("focusout",!0,!1),e.dispatchEvent(t)}function d(e){var t=document.createEvent("UIEvent");t.initEvent("modalclose",!0,!1),e.dispatchEvent(t)}function l(e,t){"invalid"==t?(c(this.dropdown,"invalid"),h(this.dropdown,"valid")):(c(this.dropdown,"valid"),h(this.dropdown,"invalid"))}function r(e,t){return null!=e[t]?e[t]:e.getAttribute(t)}function a(e,t){return!!e&&e.classList.contains(t)}function c(e,t){if(e)return e.classList.add(t)}function h(e,t){if(e)return e.classList.remove(t)}e.r(t),e.d(t,{bind:()=>f,default:()=>u});var p={data:null,searchable:!1,showSelectedItems:!1};function u(e,t){this.el=e,this.config=Object.assign({},p,t||{}),this.data=this.config.data,this.selectedOptions=[],this.placeholder=r(this.el,"placeholder")||this.config.placeholder||"Select an option",this.searchtext=r(this.el,"searchtext")||this.config.searchtext||"Search",this.selectedtext=r(this.el,"selectedtext")||this.config.selectedtext||"selected",this.dropdown=null,this.multiple=r(this.el,"multiple"),this.disabled=r(this.el,"disabled"),this.create()}function f(e,t){return new u(e,t)}return u.prototype.create=function(){this.el.style.opacity="0",this.el.style.width="0",this.el.style.padding="0",this.el.style.height="0",this.data?this.processData(this.data):this.extractData(),this.renderDropdown(),this.bindEvent()},u.prototype.processData=function(e){var t=[];e.forEach((e=>{t.push({data:e,attributes:{selected:!!e.selected,disabled:!!e.disabled,optgroup:"optgroup"==e.value}})})),this.options=t},u.prototype.extractData=function(){var e=this.el.querySelectorAll("option,optgroup"),t=[],i=[],s=[];e.forEach((e=>{if("OPTGROUP"==e.tagName)var s={text:e.label,value:"optgroup"};else s={text:e.innerText,value:e.value,selected:null!=e.getAttribute("selected")||this.el.value==e.value,disabled:null!=e.getAttribute("disabled")};var o={selected:e.selected,disabled:e.disabled,optgroup:"OPTGROUP"==e.tagName};t.push(s),i.push({data:s,attributes:o})})),this.data=t,this.options=i,this.options.forEach((e=>{e.attributes.selected&&s.push(e)})),this.selectedOptions=s},u.prototype.renderDropdown=function(){var e=["nice-select",r(this.el,"class")||"",this.disabled?"disabled":"",this.multiple?"has-multiple":""];let t='";var i=`
`;i+=``,i+='
',i+=`${this.config.searchable?t:""}`,i+='',i+="
",i+="
",this.el.insertAdjacentHTML("afterend",i),this.dropdown=this.el.nextElementSibling,this._renderSelectedItems(),this._renderItems()},u.prototype._renderSelectedItems=function(){if(this.multiple){var e="";this.config.showSelectedItems||this.config.showSelectedItems||"auto"==window.getComputedStyle(this.dropdown).width||this.selectedOptions.length<2?(this.selectedOptions.forEach((function(t){e+=`${t.data.text}`})),e=""==e?this.placeholder:e):e=this.selectedOptions.length+" "+this.selectedtext,this.dropdown.querySelector(".multiple-options").innerHTML=e}else{var t=this.selectedOptions.length>0?this.selectedOptions[0].data.text:this.placeholder;this.dropdown.querySelector(".current").innerHTML=t}},u.prototype._renderItems=function(){var e=this.dropdown.querySelector("ul");this.options.forEach((t=>{e.appendChild(this._renderItem(t))}))},u.prototype._renderItem=function(e){var t=document.createElement("li");if(t.innerHTML=e.data.text,e.attributes.optgroup)c(t,"optgroup");else{t.setAttribute("data-value",e.data.value);var i=["option",e.attributes.selected?"selected":null,e.attributes.disabled?"disabled":null];t.addEventListener("click",this._onItemClicked.bind(this,e)),t.classList.add(...i)}return e.element=t,t},u.prototype.update=function(){if(this.extractData(),this.dropdown){var e=a(this.dropdown,"open");this.dropdown.parentNode.removeChild(this.dropdown),this.create(),e&&i(this.dropdown)}r(this.el,"disabled")?this.disable():this.enable()},u.prototype.disable=function(){this.disabled||(this.disabled=!0,c(this.dropdown,"disabled"))},u.prototype.enable=function(){this.disabled&&(this.disabled=!1,h(this.dropdown,"disabled"))},u.prototype.clear=function(){this.resetSelectValue(),this.selectedOptions=[],this._renderSelectedItems(),this.update(),s(this.el)},u.prototype.destroy=function(){this.dropdown&&(this.dropdown.parentNode.removeChild(this.dropdown),this.el.style.display="")},u.prototype.bindEvent=function(){this.dropdown.addEventListener("click",this._onClicked.bind(this)),this.dropdown.addEventListener("keydown",this._onKeyPressed.bind(this)),this.dropdown.addEventListener("focusin",o.bind(this,this.el)),this.dropdown.addEventListener("focusout",n.bind(this,this.el)),this.el.addEventListener("invalid",l.bind(this,this.el,"invalid")),window.addEventListener("click",this._onClickedOutside.bind(this)),this.config.searchable&&this._bindSearchEvent()},u.prototype._bindSearchEvent=function(){var e=this.dropdown.querySelector(".nice-select-search");e&&e.addEventListener("click",(function(e){return e.stopPropagation(),!1})),e.addEventListener("input",this._onSearchChanged.bind(this))},u.prototype._onClicked=function(e){var t,i;if(e.preventDefault(),a(this.dropdown,"open")?this.multiple||(h(this.dropdown,"open"),d(this.el)):(c(this.dropdown,"open"),t=this.el,(i=document.createEvent("UIEvent")).initEvent("modalopen",!0,!1),t.dispatchEvent(i)),a(this.dropdown,"open")){var s=this.dropdown.querySelector(".nice-select-search");s&&(s.value="",s.focus());var o=this.dropdown.querySelector(".focus");h(o,"focus"),c(o=this.dropdown.querySelector(".selected"),"focus"),this.dropdown.querySelectorAll("ul li").forEach((function(e){e.style.display=""}))}else this.dropdown.focus()},u.prototype._onItemClicked=function(e,t){var i=t.target;a(i,"disabled")||(this.multiple?a(i,"selected")?(h(i,"selected"),this.selectedOptions.splice(this.selectedOptions.indexOf(e),1),this.el.querySelector(`option[value="${i.dataset.value}"]`).removeAttribute("selected")):(c(i,"selected"),this.selectedOptions.push(e)):(this.selectedOptions.forEach((function(e){h(e.element,"selected")})),c(i,"selected"),this.selectedOptions=[e]),this._renderSelectedItems(),this.updateSelectValue())},u.prototype.updateSelectValue=function(){if(this.multiple){var e=this.el;this.selectedOptions.forEach((function(t){var i=e.querySelector(`option[value="${t.data.value}"]`);i&&i.setAttribute("selected",!0)}))}else this.selectedOptions.length>0&&(this.el.value=this.selectedOptions[0].data.value);s(this.el)},u.prototype.resetSelectValue=function(){if(this.multiple){var e=this.el;this.selectedOptions.forEach((function(t){var i=e.querySelector(`option[value="${t.data.value}"]`);i&&i.removeAttribute("selected")}))}else this.selectedOptions.length>0&&(this.el.selectedIndex=-1);s(this.el)},u.prototype._onClickedOutside=function(e){this.dropdown.contains(e.target)||(h(this.dropdown,"open"),d(this.el))},u.prototype._onKeyPressed=function(e){var t=this.dropdown.querySelector(".focus"),s=a(this.dropdown,"open");if(13==e.keyCode)i(s?t:this.dropdown);else if(40==e.keyCode){if(s){var o=this._findNext(t);o&&(h(this.dropdown.querySelector(".focus"),"focus"),c(o,"focus"))}else i(this.dropdown);e.preventDefault()}else if(38==e.keyCode){if(s){var n=this._findPrev(t);n&&(h(this.dropdown.querySelector(".focus"),"focus"),c(n,"focus"))}else i(this.dropdown);e.preventDefault()}else if(27==e.keyCode&&s)i(this.dropdown);else if(32===e.keyCode&&s)return!1;return!1},u.prototype._findNext=function(e){for(e=e?e.nextElementSibling:this.dropdown.querySelector(".list .option");e;){if(!a(e,"disabled")&&"none"!=e.style.display)return e;e=e.nextElementSibling}return null},u.prototype._findPrev=function(e){for(e=e?e.previousElementSibling:this.dropdown.querySelector(".list .option:last-child");e;){if(!a(e,"disabled")&&"none"!=e.style.display)return e;e=e.previousElementSibling}return null},u.prototype._onSearchChanged=function(e){var t=a(this.dropdown,"open"),i=e.target.value;if(""==(i=i.toLowerCase()))this.options.forEach((function(e){e.element.style.display=""}));else if(t){var s=new RegExp(i);this.options.forEach((function(e){var t=e.data.text.toLowerCase(),i=s.test(t);e.element.style.display=i?"":"none"}))}this.dropdown.querySelectorAll(".focus").forEach((function(e){h(e,"focus")})),c(this._findNext(null),"focus")},t})())); \ No newline at end of file diff --git a/pkg/server/ui/dist/dist/wasm_exec.js b/pkg/server/ui/dist/dist/wasm_exec.js new file mode 100644 index 00000000..bc6f2102 --- /dev/null +++ b/pkg/server/ui/dist/dist/wasm_exec.js @@ -0,0 +1,561 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); diff --git a/pkg/server/ui/dist/index.html b/pkg/server/ui/dist/index.html new file mode 100644 index 00000000..ad538950 --- /dev/null +++ b/pkg/server/ui/dist/index.html @@ -0,0 +1,120 @@ + + + + + + KyvernoJson Playground + + + + + + + + + + + + +
+
+
+
+
+ + Input + + (YAML format) +
+ +
+
+
+
+
+
+
+ + Policy + + (YAML format) +
+ +
+
+
+
+
+ Output +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/pkg/server/ui/dist/sw.js b/pkg/server/ui/dist/sw.js new file mode 100644 index 00000000..41d4f8fc --- /dev/null +++ b/pkg/server/ui/dist/sw.js @@ -0,0 +1,14 @@ +importScripts('dist/wasm_exec.js') +importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v1.1.0/sw.js') + +// Skip installed stage and jump to activating stage +addEventListener('install', (event) => { + event.waitUntil(skipWaiting()) +}) + +// Start controlling clients as soon as the SW is activated +addEventListener('activate', event => { + event.waitUntil(clients.claim()) +}) + +registerWasmHTTPListener('assets/main.wasm', { base: 'api' }) diff --git a/pkg/server/ui/embed.go b/pkg/server/ui/embed.go new file mode 100644 index 00000000..0b36c06c --- /dev/null +++ b/pkg/server/ui/embed.go @@ -0,0 +1,8 @@ +package ui + +import ( + "embed" +) + +//go:embed dist +var staticFiles embed.FS diff --git a/pkg/server/ui/routes.go b/pkg/server/ui/routes.go new file mode 100644 index 00000000..ee06c78e --- /dev/null +++ b/pkg/server/ui/routes.go @@ -0,0 +1,20 @@ +package ui + +import ( + "io/fs" + "net/http" + + "github.com/gin-gonic/gin" +) + +func AddRoutes(router *gin.Engine) error { + fs, err := fs.Sub(staticFiles, "dist") + if err != nil { + return err + } + fileServer := http.FileServer(http.FS(fs)) + router.NoRoute(func(c *gin.Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + }) + return nil +} diff --git a/pkg/server/wasm.go b/pkg/server/wasm.go new file mode 100644 index 00000000..e807eadc --- /dev/null +++ b/pkg/server/wasm.go @@ -0,0 +1,13 @@ +//go:build js && wasm + +package server + +import ( + "context" + + wasmhttp "github.com/nlepage/go-wasm-http-server" +) + +func RunWasm(_ context.Context, s Server) { + wasmhttp.Serve(s.Handler()) +} diff --git a/pkg/server/wasm/server.go b/pkg/server/wasm/server.go deleted file mode 100644 index 9bb69798..00000000 --- a/pkg/server/wasm/server.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build js && wasm - -package wasm - -import ( - "context" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - "github.com/kyverno/kyverno-json/pkg/server/api" - "github.com/kyverno/kyverno-json/pkg/server/playground" - wasmhttp "github.com/nlepage/go-wasm-http-server" -) - -const ( - apiPrefix = "/api" - playgroundPrefix = "/playground" -) - -type Shutdown = func(context.Context) error - -type Server interface { - AddApiRoutes(api.Configuration) error - AddPlaygroundRoutes() error - Run(context.Context) Shutdown -} - -type server struct { - *gin.Engine -} - -func New(enableLogger bool, enableCors bool) (Server, error) { - router := gin.New() - if enableLogger { - router.Use(gin.Logger()) - } - router.Use(gin.Recovery()) - if enableCors { - router.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"POST", "GET", "HEAD"}, - AllowHeaders: []string{"Origin", "Content-Type"}, - ExposeHeaders: []string{"Content-Length"}, - })) - } - return server{router}, nil -} - -func (s server) AddApiRoutes(config api.Configuration) error { - return api.AddRoutes(s.Group(apiPrefix), config) -} - -func (s server) AddPlaygroundRoutes() error { - return playground.AddRoutes(s.Group(playgroundPrefix)) -} - -func (s server) Run(_ context.Context) Shutdown { - wasmhttp.Serve(s.Engine.Handler()) - return nil -} diff --git a/website/docs/commands/kyverno-json.md b/website/docs/commands/kyverno-json.md index f6260017..26e143aa 100644 --- a/website/docs/commands/kyverno-json.md +++ b/website/docs/commands/kyverno-json.md @@ -22,6 +22,7 @@ kyverno-json [flags] * [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell * [kyverno-json docs](kyverno-json_docs.md) - Generates reference documentation. * [kyverno-json jp](kyverno-json_jp.md) - Provides a command-line interface to JMESPath, enhanced with custom functions. +* [kyverno-json playground](kyverno-json_playground.md) - playground * [kyverno-json scan](kyverno-json_scan.md) - scan * [kyverno-json serve](kyverno-json_serve.md) - serve * [kyverno-json version](kyverno-json_version.md) - Prints the version informations. diff --git a/website/docs/commands/kyverno-json_playground.md b/website/docs/commands/kyverno-json_playground.md new file mode 100644 index 00000000..11519777 --- /dev/null +++ b/website/docs/commands/kyverno-json_playground.md @@ -0,0 +1,28 @@ +## kyverno-json playground + +playground + +### Synopsis + +Serve playground + +``` +kyverno-json playground [flags] +``` + +### Options + +``` + --gin-cors enable gin cors (default true) + --gin-log enable gin logger (default true) + --gin-max-body-size int gin max body size (default 2097152) + --gin-mode string gin run mode (default "release") + -h, --help help for playground + --server-host string server host (default "0.0.0.0") + --server-port int server port (default 8080) +``` + +### SEE ALSO + +* [kyverno-json](kyverno-json.md) - kyverno-json is a CLI tool to apply policies to json resources. + diff --git a/website/mkdocs.yaml b/website/mkdocs.yaml index f69fe7e0..8c3fdc16 100644 --- a/website/mkdocs.yaml +++ b/website/mkdocs.yaml @@ -27,6 +27,7 @@ nav: - commands/kyverno-json_jp_function.md - commands/kyverno-json_jp_parse.md - commands/kyverno-json_jp_query.md + - commands/kyverno-json_playground.md - commands/kyverno-json_scan.md - commands/kyverno-json_serve.md - commands/kyverno-json_version.md diff --git a/website/nav.gotmpl b/website/nav.gotmpl index c36c09b2..babc7809 100644 --- a/website/nav.gotmpl +++ b/website/nav.gotmpl @@ -27,6 +27,7 @@ nav: - commands/kyverno-json_jp_function.md - commands/kyverno-json_jp_parse.md - commands/kyverno-json_jp_query.md + - commands/kyverno-json_playground.md - commands/kyverno-json_scan.md - commands/kyverno-json_serve.md - commands/kyverno-json_version.md