From 608295c949a4202467d0dba702855cafd75fc792 Mon Sep 17 00:00:00 2001 From: Jasmin Gacic Date: Tue, 22 Nov 2022 13:07:09 +0100 Subject: [PATCH] first run at overlays (#966) * first run at overlays Signed-off-by: jasmingacic * some more work on overlays Signed-off-by: jasmingacic * checking for doc type before parsin openapi specs Signed-off-by: jasmingacic * added overlay parameter to generate and deploy Signed-off-by: jasmingacic * cleaned up overlays_test.go Signed-off-by: jasmingacic * some minor clean up Signed-off-by: jasmingacic * addressed PR remarks and extended overlay_test.go Signed-off-by: jasmingacic * refactoring and docs Signed-off-by: jasmingacic * go mod tidy Signed-off-by: jasmingacic * changed docker image Signed-off-by: jasmingacic * revert manager_auth_proxy_patch.yaml Signed-off-by: jasmingacic * revert manager_auth_proxy_patch.yaml Signed-off-by: jasmingacic * refactoring Signed-off-by: jasmingacic * build fix Signed-off-by: jasmingacic Signed-off-by: jasmingacic --- cmd/kusk/cmd/deploy.go | 58 ++++--- cmd/kusk/cmd/generate.go | 110 ++++-------- cmd/kusk/cmd/ip.go | 1 - cmd/kusk/cmd/manifest_data.go | 2 +- cmd/kusk/cmd/root.go | 98 ++++++----- cmd/kusk/cmd/validate.go | 6 +- cmd/kusk/internal/overlays/overlay.yaml | 12 ++ cmd/kusk/internal/overlays/overlays.go | 173 +++++++++++++++++++ cmd/kusk/internal/overlays/overlays_test.go | 24 +++ config/default/manager_auth_proxy_patch.yaml | 2 +- docs/docs/reference/cli/generate-cmd.md | 14 +- pkg/spec/spec.go | 19 +- 12 files changed, 369 insertions(+), 150 deletions(-) create mode 100644 cmd/kusk/internal/overlays/overlay.yaml create mode 100644 cmd/kusk/internal/overlays/overlays.go create mode 100644 cmd/kusk/internal/overlays/overlays_test.go diff --git a/cmd/kusk/cmd/deploy.go b/cmd/kusk/cmd/deploy.go index cc7aa630f..4169ad835 100644 --- a/cmd/kusk/cmd/deploy.go +++ b/cmd/kusk/cmd/deploy.go @@ -45,6 +45,7 @@ import ( "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/errors" "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/kuskui" "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/mocking/filewatcher" + "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/overlays" "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/utils" "github.com/kubeshop/kusk-gateway/cmd/kusk/templates" "github.com/kubeshop/kusk-gateway/pkg/options" @@ -52,7 +53,6 @@ import ( ) var ( - file string watch bool ) @@ -60,16 +60,15 @@ func init() { //add to root command rootCmd.AddCommand(deployCmd) - deployCmd.Flags().StringVarP(&file, "in", "i", "", "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml") - deployCmd.MarkFlagRequired("in") - + deployCmd.Flags().StringVarP(&apiSpecPath, "in", "i", "", "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml") deployCmd.Flags().BoolVarP(&watch, "watch", "w", false, "watch file changes and deploy on change") - deployCmd.Flags().StringVar(&name, "name", "", "name of the API") - deployCmd.Flags().StringVar(&namespace, "namespace", "default", "name of the API") + deployCmd.Flags().StringVar(&name, "name", "", "the name of the API resource") + deployCmd.Flags().StringVar(&namespace, "namespace", "default", "the namespace of the API resource") deployCmd.Flags().StringVarP(&envoyFleetName, "envoyfleet.name", "", "kusk-gateway-envoy-fleet", "name of envoyfleet to use for this API. Default: kusk-gateway-envoy-fleet") - deployCmd.Flags().StringVarP(&envoyFleetNamespace, "envoyfleet.namespace", "", kusknamespace, "namespace of envoyfleet to use for this API. Default: kusk-system") + deployCmd.Flags().StringVarP(&overlaySpecPath, "overlay", "", "", "file path or URL to Overlay spec file to generate mappings from. e.g. --overlay overlay.yaml") + } // apiCmd represents the api command @@ -86,13 +85,13 @@ var deployCmd = &cobra.Command{ } } - originalManifest, err := getParsedAndValidatedOpenAPISpec(file) + originalManifest, err := getParsedAndValidatedOpenAPISpec(overlaySpecPath, apiSpecPath) if err != nil { reportError(err) return err } - kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", file)) + kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", apiSpecPath)) kuskui.PrintStart(fmt.Sprintf("initiallizing deployment to fleet %s", envoyFleetName)) k8sclient, err := utils.GetK8sClient() @@ -137,11 +136,11 @@ var deployCmd = &cobra.Command{ kuskui.PrintInfo(fmt.Sprintf("api.gateway.kusk.io/%s created\n", api.Name)) } - if _, e := url.ParseRequestURI(file); e != nil { + if _, e := url.ParseRequestURI(apiSpecPath); e != nil { if watch { var watcher *filewatcher.FileWatcher - watcher, err = filewatcher.New(file) + watcher, err = filewatcher.New(apiSpecPath) if err != nil { reportError(err) return err @@ -152,13 +151,13 @@ var deployCmd = &cobra.Command{ signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) if watcher != nil { - kuskui.PrintInfo(fmt.Sprintf("⏳ watching for API changes in %s", file)) + kuskui.PrintInfo(fmt.Sprintf("⏳ watching for API changes in %s", apiSpecPath)) go watcher.Watch(func() { - kuskui.PrintStart(fmt.Sprintf("✍️ change detected in %s", file)) - kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", file)) + kuskui.PrintStart(fmt.Sprintf("✍️ change detected in %s", apiSpecPath)) + kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", apiSpecPath)) kuskui.PrintStart(fmt.Sprintf("initiallizing deployment to fleet %s", envoyFleetName)) - manifest, err := getParsedAndValidatedOpenAPISpec(file) + manifest, err := getParsedAndValidatedOpenAPISpec(overlaySpecPath, apiSpecPath) if err != nil { reportError(err) kuskui.PrintError(err.Error()) @@ -203,12 +202,32 @@ var deployCmd = &cobra.Command{ }, } -func getParsedAndValidatedOpenAPISpec(apiSpecPath string) (string, error) { +func getParsedAndValidatedOpenAPISpec(overlaySpecPath, apiSpecPath string) (string, error) { const KuskExtensionKey = "x-kusk" - parsedApiSpec, err := spec.NewParser(&openapi3.Loader{IsExternalRefsAllowed: true}).Parse(apiSpecPath) - if err != nil { - return "", err + parsedApiSpec := &openapi3.T{} + var err error + + if overlaySpecPath != "" { + overlay, err := overlays.NewOverlay(overlaySpecPath) + if err != nil { + return "", err + } + + overlayPath, err := overlay.Apply() + if err != nil { + return "", err + } + + parsedApiSpec, err = spec.NewParser(&openapi3.Loader{IsExternalRefsAllowed: true}).Parse(overlayPath) + if err != nil { + return "", err + } + } else { + parsedApiSpec, err = spec.NewParser(&openapi3.Loader{IsExternalRefsAllowed: true}).Parse(apiSpecPath) + if err != nil { + return "", err + } } if _, ok := parsedApiSpec.ExtensionProps.Extensions[KuskExtensionKey]; !ok { @@ -226,7 +245,6 @@ func getParsedAndValidatedOpenAPISpec(apiSpecPath string) (string, error) { if err != nil { return "", err } - if err := opts.Validate(); err != nil { return "", err } diff --git a/cmd/kusk/cmd/generate.go b/cmd/kusk/cmd/generate.go index e9fe2b85e..96b134ec0 100644 --- a/cmd/kusk/cmd/generate.go +++ b/cmd/kusk/cmd/generate.go @@ -33,14 +33,16 @@ import ( "github.com/spf13/cobra" "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/errors" + "github.com/kubeshop/kusk-gateway/cmd/kusk/internal/overlays" "github.com/kubeshop/kusk-gateway/cmd/kusk/templates" "github.com/kubeshop/kusk-gateway/pkg/options" "github.com/kubeshop/kusk-gateway/pkg/spec" ) var ( - apiTemplate *template.Template - apiSpecPath string + apiTemplate *template.Template + apiSpecPath string + overlaySpecPath string name string namespace string @@ -66,11 +68,31 @@ var generateCmd = &cobra.Command{ errors.NewErrorReporter(cmd, err).Report() } } + parsedApiSpec := &openapi3.T{} + parser := spec.NewParser(&openapi3.Loader{IsExternalRefsAllowed: true}) + var err error - parsedApiSpec, err := spec.NewParser(&openapi3.Loader{IsExternalRefsAllowed: true}).Parse(apiSpecPath) - if err != nil { - reportError(err) - return err + if overlaySpecPath != "" { + overlay, err := overlays.NewOverlay(overlaySpecPath) + if err != nil { + return err + } + overlayPath, err := overlay.Apply() + if err != nil { + return err + } + + parsedApiSpec, err = parser.Parse(overlayPath) + if err != nil { + reportError(err) + return err + } + } else if apiSpecPath != "" { + parsedApiSpec, err = parser.Parse(apiSpecPath) + if err != nil { + reportError(err) + return err + } } if _, ok := parsedApiSpec.ExtensionProps.Extensions["x-kusk"]; !ok { @@ -170,74 +192,18 @@ func getAPISpecString(apiSpec *openapi3.T) (string, error) { func init() { rootCmd.AddCommand(generateCmd) - // This should be deprecated soon. - // See `apiCmd.Deprecated`. apiCmd.AddCommand(generateCmd) - generateCmd.Flags().StringVarP( - &name, - "name", - "", - "", - "the name to give the API resource e.g. --name my-api", - ) - - generateCmd.Flags().StringVarP( - &namespace, - "namespace", - "n", - "default", - "the namespace of the API resource e.g. --namespace my-namespace, -n my-namespace", - ) - - generateCmd.Flags().StringVarP( - &apiSpecPath, - "in", - "i", - "", - "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml", - ) - generateCmd.MarkFlagRequired("in") - - generateCmd.Flags().StringVarP( - &serviceName, - "upstream.service", - "", - "", - "name of upstream service", - ) - - generateCmd.Flags().StringVarP( - &serviceNamespace, - "upstream.namespace", - "", - "default", - "namespace of upstream service", - ) - - generateCmd.Flags().Uint32VarP( - &servicePort, - "upstream.port", - "", - 80, - "port of upstream service", - ) - - generateCmd.Flags().StringVarP( - &envoyFleetName, - "envoyfleet.name", - "", - "kusk-gateway-envoy-fleet", - "name of envoyfleet to use for this API. Default: kusk-gateway-envoy-fleet", - ) - - generateCmd.Flags().StringVarP( - &envoyFleetNamespace, - "envoyfleet.namespace", - "", - kusknamespace, - "namespace of envoyfleet to use for this API. Default: kusk-system", - ) + generateCmd.Flags().StringVarP(&name, "name", "", "", "the name to give the API resource e.g. --name my-api") + generateCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "the namespace of the API resource e.g. --namespace my-namespace, -n my-namespace") + generateCmd.Flags().StringVarP(&apiSpecPath, "in", "i", "", "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml") + + generateCmd.Flags().StringVarP(&serviceName, "upstream.service", "", "", "name of upstream service") + generateCmd.Flags().StringVarP(&serviceNamespace, "upstream.namespace", "", "default", "namespace of upstream service") + generateCmd.Flags().Uint32VarP(&servicePort, "upstream.port", "", 80, "port of upstream service") + generateCmd.Flags().StringVarP(&envoyFleetName, "envoyfleet.name", "", "kusk-gateway-envoy-fleet", "name of envoyfleet to use for this API. Default: kusk-gateway-envoy-fleet") + generateCmd.Flags().StringVarP(&envoyFleetNamespace, "envoyfleet.namespace", "", kusknamespace, "namespace of envoyfleet to use for this API. Default: kusk-system") + generateCmd.Flags().StringVarP(&overlaySpecPath, "overlay", "", "", "file path or URL to Overlay spec file to generate mappings from. e.g. --overlay overlay.yaml") apiTemplate = template.Must(template.New("api").Parse(templates.APITemplate)) } diff --git a/cmd/kusk/cmd/ip.go b/cmd/kusk/cmd/ip.go index 74b02b84c..04105a8c4 100644 --- a/cmd/kusk/cmd/ip.go +++ b/cmd/kusk/cmd/ip.go @@ -128,6 +128,5 @@ var ipCmd = &cobra.Command{ return } fmt.Println(ip) - return }, } diff --git a/cmd/kusk/cmd/manifest_data.go b/cmd/kusk/cmd/manifest_data.go index d62d9e05a..4f43fa192 100644 --- a/cmd/kusk/cmd/manifest_data.go +++ b/cmd/kusk/cmd/manifest_data.go @@ -382,7 +382,7 @@ func configDefaultManager_webhook_patchYaml() (*asset, error) { return a, nil } -var _configManagerConfigmapYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x91\x41\x6f\xdb\x20\x18\x86\xef\xfc\x8a\x4f\xb9\xe3\xd8\xd9\x61\x11\x37\x62\xb3\xc4\x1a\x81\x8a\x5a\x99\x7a\x42\x24\xfd\xea\x59\x8e\xb1\x65\x88\xab\xfe\xfb\x09\x6d\x9d\xda\x4d\x95\x7a\x02\xc1\xf3\xf0\xbd\xe2\xa5\x94\x12\x37\x75\x27\x9c\x43\x37\x7a\x06\x4b\x41\x1e\x5d\x74\x8c\x00\xf0\xbd\x50\x8d\x3d\x72\xc5\xf7\xc2\xd8\x5d\xad\x2a\xcb\xab\xca\x30\x60\xc5\x36\x2f\xf2\x44\x28\x2e\x1f\x9a\xba\xbc\xb7\x42\xf1\x9d\x14\x15\x83\x55\x9c\x6f\xb8\x22\x00\xbf\x4f\xac\x14\xbc\x12\xc6\x0a\x29\xca\xa6\xd6\x8a\xc1\xea\xc9\x5d\xc3\x1f\xe2\xa4\x1f\x6c\xa9\x55\x63\xb4\xb4\x77\x92\x2b\xf1\xef\x94\x3c\x4d\x39\x08\x2e\x9b\x83\xbd\x33\x7a\xf7\x1e\xd8\xe6\xdb\x82\x00\x48\xbd\xb7\x52\x9c\x84\x64\x50\xab\x6f\x9a\x00\x1c\x45\x63\x52\xaa\x37\x70\xb1\xf9\x9a\xe5\x59\x9e\x15\xc9\x4a\xaf\xfe\x10\xbb\x83\xd6\xdf\x6d\x29\x4c\x73\x6f\xab\xda\x30\x58\xc7\x61\x5a\xf7\xdb\x40\x9f\xf1\xfc\x73\x1c\x7b\x1a\x70\x5e\x70\x5e\xa7\xa5\xf3\x2d\xbd\xe0\x1c\x03\xe9\x3b\xff\xc8\xa0\x1c\xfd\x53\xd7\x1e\xdd\x44\x06\x8c\xee\xf5\xcb\xae\xee\x8c\xd7\x90\x76\x00\x6e\x9a\xb2\xfe\x76\xc6\xd9\x63\xc4\x90\x75\xe3\xfa\x32\x0e\xd3\xe8\xd1\x47\x06\xfd\x2d\xf4\xb4\x75\x11\x9f\xdd\x0b\x1d\x9c\x77\x2d\xce\x1f\x58\x9d\x0f\xd1\xf9\x0b\xbe\x97\x3e\x80\xbd\x1b\x3e\x07\x2e\xaf\x85\xaf\x96\x22\xfb\x92\x6d\x36\xa9\x91\xff\xed\x37\xd9\xd2\x65\x98\xdc\xdf\x20\xe1\x25\x44\x1c\xc8\xaf\x00\x00\x00\xff\xff\xb1\x69\x78\x70\x41\x02\x00\x00") +var _configManagerConfigmapYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x91\x41\x6f\xdb\x20\x18\x86\xef\xfc\x8a\x4f\xb9\xe3\x98\xee\xb0\x88\x1b\xb1\x59\x62\x8d\x40\x45\xad\x4c\x3d\x21\x92\x7e\xcd\x2c\xc7\xd8\x32\x24\x55\xff\xfd\x84\xb6\x4e\xed\xa6\x48\x3d\x81\xe0\x79\xf8\x5e\xf1\x52\x4a\x89\x9f\xba\x3d\xce\xb1\x1b\x03\x87\x2b\x23\x4f\x3e\x79\x4e\x00\xc4\x46\xea\xd6\xed\x84\x16\x1b\x69\xdd\xba\xd1\xb5\x13\x75\x6d\x39\x70\xb6\x2a\x59\x99\x09\x2d\xd4\x63\xdb\x54\x0f\x4e\x6a\xb1\x56\xb2\xe6\xb0\x48\xf3\x05\x17\x04\xe0\xf7\x89\x53\x52\xd4\xd2\x3a\xa9\x64\xd5\x36\x46\x73\x58\x3c\xfb\x73\xfc\x43\xec\xcd\xa3\xab\x8c\x6e\xad\x51\xee\x5e\x09\x2d\xff\x9d\x52\xe6\x29\x5b\x29\x54\xbb\x75\xf7\xd6\xac\x3f\x02\xab\x72\xc5\x08\x80\x32\x1b\xa7\xe4\x5e\x2a\x0e\x8d\xfe\x66\x08\xc0\x4e\xb6\x36\xa7\x7a\x07\xb3\xbb\xaf\x45\x59\x94\x05\xcb\x56\x7e\xf5\x87\x5c\x6f\x8d\xf9\xee\x2a\x69\xdb\x07\x57\x37\x96\xc3\x32\x0d\xd3\xb2\x5f\x45\xfa\x82\x87\x9f\xe3\xd8\xd3\x88\xf3\x15\xe7\x65\x5e\xba\x70\xa2\x47\x9c\x53\x24\x7d\x17\x9e\x38\x54\x63\x78\xee\x4e\x3b\x3f\x91\x01\x93\x7f\xfb\xb2\xb3\x3f\xe0\x39\xe6\x1d\x80\x9f\xa6\xa2\xbf\x1c\x70\x0e\x98\x30\x16\xdd\xb8\x3c\x8e\xc3\x34\x06\x0c\x89\x43\x7f\x89\x3d\x3d\xf9\x84\x2f\xfe\x95\x0e\x3e\xf8\x13\xce\x37\xac\x2e\xc4\xe4\xc3\x11\x3f\x4a\x37\xe0\xe0\x87\xcf\x81\xd7\xb7\xc2\x17\x57\x56\x7c\x29\xee\x58\x6e\xe4\x7f\xfb\x5d\xb6\x7c\x19\x27\xff\x37\x48\x7c\x8d\x09\x07\xf2\x2b\x00\x00\xff\xff\x1d\x1f\x87\x59\x41\x02\x00\x00") func configManagerConfigmapYamlBytes() ([]byte, error) { return bindataRead( diff --git a/cmd/kusk/cmd/root.go b/cmd/kusk/cmd/root.go index ac7a33da3..85abb8cb2 100644 --- a/cmd/kusk/cmd/root.go +++ b/cmd/kusk/cmd/root.go @@ -59,53 +59,21 @@ var rootCmd = &cobra.Command{ Use: "kusk", Short: "", Long: ``, - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { analytics.SendAnonymousCMDInfo(nil) - if isatty.IsTerminal(os.Stdout.Fd()) == true && build.Version != "latest" { + versionCheck(cmd) - if len(build.Version) != 0 { - ghclient, err := utils.NewGithubClient("", nil) - if err != nil { - errors.NewErrorReporter(cmd, err).Report() - return - } - - ref, err := ghclient.GetLatest(kuskgateway) - if err != nil { - errors.NewErrorReporter(cmd, err).Report() - return - } - - latestVersion, err := version.NewVersion(ref) - if err != nil { - errors.NewErrorReporter(cmd, err).Report() - return - } - - currentVersion, err := version.NewVersion(build.Version) - if err != nil { - errors.NewErrorReporter(cmd, err).Report() - return - } - - if currentVersion != nil && currentVersion.LessThan(latestVersion) { - kuskui.PrintWarning(fmt.Sprintf("This version %s of Kusk cli is outdated. The latest version available is %s", currentVersion, latestVersion)) - - if runtime.GOOS == "windows" { - kuskui.PrintWarning(fmt.Sprintf("Run the following command to update Kusk CLI. \n\n go install -x github.com/kubeshop/kusk-gateway/cmd/kusk@latest \n kusk cluster upgrade\n")) - } - if runtime.GOOS == "linux" { - kuskui.PrintWarning(fmt.Sprintf("Run the following command to update Kusk CLI. \n\n curl -sSLf https://raw.githubusercontent.com/kubeshop/kusk-gateway/main/cmd/kusk/scripts/install.sh | bash \n kusk cluster upgrade\n")) - } - if runtime.GOOS == "darwin" { - kuskui.PrintWarning(fmt.Sprintf("Run the following command to update Kusk CLI. \n\n brew install kubeshop/kusk/kusk \n kusk cluster upgrade\n")) - } + if cmd.Use == "generate" || cmd.Use == "deploy" { + if apiSpecPath != "" && overlaySpecPath != "" { + return fmt.Errorf(`'-i, --in and --overlay are mutually exclusive`) + } - return - } + if apiSpecPath == "" && overlaySpecPath == "" { + return fmt.Errorf(`either '-i, --in or --overlay need to be provided`) } } + return nil }, } @@ -115,9 +83,6 @@ func Execute() { err := rootCmd.Execute() if err != nil { errors.NewErrorReporter(rootCmd, err).Report() - } - - if err != nil { kuskui.PrintError(err.Error()) os.Exit(1) } @@ -270,3 +235,48 @@ func help(c *cobra.Command, s []string) { fmt.Println("") } + +func versionCheck(cmd *cobra.Command) { + if isatty.IsTerminal(os.Stdout.Fd()) && build.Version != "latest" { + if len(build.Version) != 0 { + ghclient, err := utils.NewGithubClient("", nil) + if err != nil { + errors.NewErrorReporter(cmd, err).Report() + return + } + + ref, err := ghclient.GetLatest(kuskgateway) + if err != nil { + errors.NewErrorReporter(cmd, err).Report() + return + } + + latestVersion, err := version.NewVersion(ref) + if err != nil { + errors.NewErrorReporter(cmd, err).Report() + return + } + + currentVersion, err := version.NewVersion(build.Version) + if err != nil { + errors.NewErrorReporter(cmd, err).Report() + return + } + + if currentVersion != nil && currentVersion.LessThan(latestVersion) { + kuskui.PrintWarning(fmt.Sprintf("This version %s of Kusk cli is outdated. The latest version available is %s", currentVersion, latestVersion)) + + switch runtime.GOOS { + case "windows": + kuskui.PrintWarning("Run the following command to update Kusk CLI. \n\n go install -x github.com/kubeshop/kusk-gateway/cmd/kusk@latest \n kusk cluster upgrade\n") + case "linux": + kuskui.PrintWarning("Run the following command to update Kusk CLI. \n\n curl -sSLf https://raw.githubusercontent.com/kubeshop/kusk-gateway/main/cmd/kusk/scripts/install.sh | bash \n kusk cluster upgrade\n") + case "darwin": + kuskui.PrintWarning("Run the following command to update Kusk CLI. \n\n brew install kubeshop/kusk/kusk \n kusk cluster upgrade\n") + } + + return + } + } + } +} diff --git a/cmd/kusk/cmd/validate.go b/cmd/kusk/cmd/validate.go index 4ca02c966..3e1dbce05 100644 --- a/cmd/kusk/cmd/validate.go +++ b/cmd/kusk/cmd/validate.go @@ -44,13 +44,13 @@ var validateCmd = &cobra.Command{ } } - _, err := getParsedAndValidatedOpenAPISpec(file) + _, err := getParsedAndValidatedOpenAPISpec(overlaySpecPath, apiSpecPath) if err != nil { reportError(err) return err } - kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", file)) + kuskui.PrintSuccess(fmt.Sprintf("successfully parsed %s", apiSpecPath)) return nil }, @@ -59,6 +59,6 @@ var validateCmd = &cobra.Command{ func init() { rootCmd.AddCommand(validateCmd) - validateCmd.Flags().StringVarP(&file, "in", "i", "", "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml") + validateCmd.Flags().StringVarP(&apiSpecPath, "in", "i", "", "file path or URL to OpenAPI spec file to generate mappings from. e.g. --in apispec.yaml") validateCmd.MarkFlagRequired("file") } diff --git a/cmd/kusk/internal/overlays/overlay.yaml b/cmd/kusk/internal/overlays/overlay.yaml new file mode 100644 index 000000000..13b43cdd6 --- /dev/null +++ b/cmd/kusk/internal/overlays/overlay.yaml @@ -0,0 +1,12 @@ +overlays: 1.0.0 +extends: https://petstore3.swagger.io/api/v3/openapi.json +actions: + - target: "$.paths" + remove: true + - target: "$.components" + remove: true + - target: "$.info" + update: + title: Overlayed! + description: | + This API has been overlayed 😎 diff --git a/cmd/kusk/internal/overlays/overlays.go b/cmd/kusk/internal/overlays/overlays.go new file mode 100644 index 000000000..20671812e --- /dev/null +++ b/cmd/kusk/internal/overlays/overlays.go @@ -0,0 +1,173 @@ +package overlays + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "sigs.k8s.io/yaml" +) + +const imageName = "kubeshop/overlay-cli" + +type Overlay struct { + Overlays string `json:"overlays,omitempty" yaml:"overlays,omitempty"` + Extends string `json:"extends,omitempty" yaml:"extends,omitempty"` + Actions []Action `json:"actions,omitempty" yaml:"actions,omitempty"` + path string + url string +} + +type Action struct { + Target string `json:"target,omitempty" yaml:"target,omitempty"` + Remove bool `json:"remove,omitempty" yaml:"remove,omitempty"` + Update interface{} `json:"update,omitempty" yaml:"update,omitempty"` + Where interface{} `json:"where,omitempty" yaml:"where,omitempty"` +} + +func NewOverlay(path string) (o *Overlay, err error) { + o = &Overlay{} + if !IsUrl(path) { + dat, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + if err := yaml.UnmarshalStrict(dat, o); err != nil { + return nil, err + } + overlay, err := os.CreateTemp("", "overlay") + if err != nil { + return nil, err + } else { + if _, err := overlay.Write(dat); err != nil { + return nil, err + } + } + o.path = overlay.Name() + + return o, nil + } + return getFile(path) +} + +func (o *Overlay) Apply() (string, error) { + var err error + var overlayed string + if !IsUrl(o.Extends) { + overlayed, err = applyOverlay(o.path, o.Extends) + if err != nil { + return "", err + } + } else { + overlayed, err = applyOverlay(o.path, "") + if err != nil { + return "", err + } + } + if f, err := os.CreateTemp("", "overlay"); err != nil { + return "", err + } else { + if _, err := f.Write([]byte(overlayed)); err != nil { + return "", err + } + return f.Name(), err + } +} + +func getFile(url string) (overlay *Overlay, err error) { + // Get the data + overlay = &Overlay{} + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bad status: %s", resp.Status) + } + + // Writer the body to file + o, err := io.ReadAll(resp.Body) + + if err != nil { + return overlay, err + } + + if err := yaml.UnmarshalStrict(o, overlay); err != nil { + return nil, err + } + + if ov, err := os.CreateTemp("", "overlay"); err != nil { + return nil, err + } else { + if _, err := ov.Write(o); err != nil { + return nil, err + } + + overlay.url = url + overlay.path = ov.Name() + + return overlay, nil + } +} + +func applyOverlay(path string, extends string) (string, error) { + abs, _ := filepath.Abs(path) + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return "", err + } + defer cli.Close() + reader, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{}) + if err != nil { + return "", err + } + + defer reader.Close() + io.Copy(io.Discard, reader) + + volumes := fmt.Sprintf("-v=%s:/overlay.yaml", abs) + var extendVolume string + if len(extends) > 0 { + if FileExists(extends) { + base := filepath.Base(extends) + extendsAbs, _ := filepath.Abs(extends) + extendVolume = fmt.Sprintf("-v=%s:/%s", extendsAbs, base) + } else { + return "", fmt.Errorf(fmt.Sprintf("%s file does not exist", extends)) + } + } + + var cmd *exec.Cmd + if len(extendVolume) > 0 { + cmd = exec.Command("docker", "run", "--rm", volumes, extendVolume, imageName) + } else { + cmd = exec.Command("docker", "run", "--rm", volumes, imageName) + } + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + return string(out), nil +} + +func IsUrl(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +func FileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/cmd/kusk/internal/overlays/overlays_test.go b/cmd/kusk/internal/overlays/overlays_test.go new file mode 100644 index 000000000..9eb4febe8 --- /dev/null +++ b/cmd/kusk/internal/overlays/overlays_test.go @@ -0,0 +1,24 @@ +package overlays + +import ( + "testing" +) + +func TestOverlayPass(t *testing.T) { + overlay, err := NewOverlay("overlay.yaml") + if err != nil { + t.Log(err) + t.Fail() + } + + if _, err := overlay.Apply(); err != nil { + t.Log(err) + t.Fail() + } +} + +func TestOverlayNoPass(t *testing.T) { + if _, err := NewOverlay("overlayed.yaml"); err == nil { + t.Fail() + } +} diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index 64598b369..01192ff32 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -25,4 +25,4 @@ spec: - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" - "--log-level=INFO" - - "--development" + - "--development" \ No newline at end of file diff --git a/docs/docs/reference/cli/generate-cmd.md b/docs/docs/reference/cli/generate-cmd.md index 4bddfa8b8..74e1381ff 100644 --- a/docs/docs/reference/cli/generate-cmd.md +++ b/docs/docs/reference/cli/generate-cmd.md @@ -1,6 +1,6 @@ # `kusk generate` -The `generate` command accepts your OpenAPI definition as input, either as a local file or a URL pointing to your file +The `generate` command accepts your OpenAPI or Overlays definition as input, either as a local file or a URL pointing to your file and generates a Kusk Gateway compatible API resource that you can apply directly into your cluster. Use this command to automate API deployment workflows from an existing OpenAPI definition. @@ -29,6 +29,7 @@ kusk api generate \ --envoyfleet.namespace kusk-system ``` + In the above example, Kusk will use the OpenAPI definition `info.title` property to generate a manifest name and leave the existing `x-kusk` extension settings. @@ -96,14 +97,15 @@ x-kusk: ``` #### **Arguments** -| Argument | Description | Required? | -|:--------------------------|:----------------------------------------------------------------------------------------------------|:---------:| -| `--name` | The name to give the API resource e.g. --name my-api. Otherwise, taken from OpenAPI info title field. | ❌ | -| `--namespace` / `-n` | The namespace of the API resource e.g. --namespace my-namespace, -n my-namespace (default: default). | ❌ | -| `--in` / `-i` | The file path or URL to OpenAPI definition to generate mappings from. e.g. --in apispec.yaml. | ✅ | +| Argument | Description | Required? | +|:--------------------------|:---------------------------------------------------------------------------------------------------------|:---------:| +| `--name` | The name to give the API resource e.g. --name my-api. Otherwise, taken from OpenAPI info title field. | ❌ | +| `--namespace` / `-n` | The namespace of the API resource e.g. --namespace my-namespace, -n my-namespace (default: default). | ❌ | +| `--in` / `-i` | The file path or URL to OpenAPI definition to generate mappings from. e.g. --in apispec.yaml. | ✅ | | `--upstream.service` | The name of upstream Kubernetes service. | ❌ | | `--upstream.namespace` | The namespace of upstream service (default: default). | ❌ | | `--upstream.port` | The port that upstream service is exposed on (default: 80). | ❌ | +| `--overlay` | The file path or URL to OpenAPI definition to generate mappings from. e.g. --overlay overlay.yaml | ✅ | | `--envoyfleet.name` | The name of envoyfleet to use for this API. | ✅ | | `envoyfleet.namespace` | The namespace of envoyfleet to use for this API. Default: kusk-system. | ❌ | diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index fbdd888ab..810fb6004 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -50,6 +50,19 @@ func isSwagger(spec []byte) bool { return header.Swagger != "" } +func isOpenAPI(spec []byte) bool { + // internal agent struct to help us differentiate + // between openapi spec 2.0 (swagger) and openapi 3+ + var header struct { + Swagger string `json:"swagger"` + OpenAPI string `json:"openapi"` // we might need that later to distinguish 3.1.x vs 3.0.x + } + + _ = yaml.Unmarshal(spec, &header) + + return header.OpenAPI != "" +} + type Loader interface { LoadFromURI(location *url.URL) (*openapi3.T, error) LoadFromFile(location string) (*openapi3.T, error) @@ -107,8 +120,10 @@ func (p Parser) ParseFromReader(contents io.Reader) (*openapi3.T, error) { if isSwagger(spec) { return parseSwagger(spec) } - - return parseOpenAPI3(spec) + if isOpenAPI(spec) { + return parseOpenAPI3(spec) + } + return nil, fmt.Errorf("provided specs are not OpenAPI/Swagger specs") } func parseSwagger(spec []byte) (*openapi3.T, error) {