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='