From 3e4ba2fab87e41cb1cd460550e5a25566f0c3443 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:07:51 +0200 Subject: [PATCH] feat: add json schema generation --- cmd/crd.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/generate.go | 112 +------------------------------------------ cmd/schema.go | 65 +++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 111 deletions(-) create mode 100644 cmd/crd.go create mode 100644 cmd/schema.go diff --git a/cmd/crd.go b/cmd/crd.go new file mode 100644 index 0000000..06294f8 --- /dev/null +++ b/cmd/crd.go @@ -0,0 +1,125 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + + "github.com/Skarlso/crd-to-sample-yaml/pkg" +) + +const ( + FormatHTML = "html" + FormatYAML = "yaml" +) + +// crdCmd is the command that generates CRD output. +var crdCmd = &cobra.Command{ + Use: "crd", + Short: "Simply generate a CRD output.", + RunE: runGenerate, +} + +type Handler interface { + CRDs() ([]*v1beta1.CustomResourceDefinition, error) +} + +func init() { + generateCmd.AddCommand(crdCmd) +} + +func runGenerate(_ *cobra.Command, _ []string) error { + crdHandler, err := constructHandler(args) + if err != nil { + return err + } + + if args.format == FormatHTML { + if err := pkg.LoadTemplates(); err != nil { + return fmt.Errorf("failed to load templates: %w", err) + } + } + + // determine location of output + if args.output == "" { + loc, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine executable location: %w", err) + } + + args.output = filepath.Dir(loc) + } + + crds, err := crdHandler.CRDs() + if err != nil { + return fmt.Errorf("failed to load CRDs: %w", err) + } + + var w io.WriteCloser + + if args.format == FormatHTML { + if args.stdOut { + w = os.Stdout + } else { + w, err = os.Create(args.output) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + + defer w.Close() + } + + return pkg.RenderContent(w, crds, args.comments, args.minimal) + } + + var errs []error //nolint:prealloc // nope + for _, crd := range crds { + if args.stdOut { + w = os.Stdout + } else { + outputLocation := filepath.Join(args.output, crd.Name+"_sample."+args.format) + // closed later during render + outputFile, err := os.Create(outputLocation) + if err != nil { + errs = append(errs, fmt.Errorf("failed to create file at: '%s': %w", outputLocation, err)) + + continue + } + + w = outputFile + } + + errs = append(errs, pkg.Generate(crd, w, args.comments, args.minimal, args.skipRandom)) + } + + return errors.Join(errs...) +} + +func constructHandler(args *rootArgs) (Handler, error) { + var crdHandler Handler + + switch { + case args.fileLocation != "": + crdHandler = &FileHandler{location: args.fileLocation} + case args.folderLocation != "": + crdHandler = &FolderHandler{location: args.folderLocation} + case args.url != "": + crdHandler = &URLHandler{ + url: args.url, + username: args.username, + password: args.password, + token: args.token, + } + } + + if crdHandler == nil { + return nil, errors.New("one of the flags (file, folder, url) must be set") + } + + return crdHandler, nil +} diff --git a/cmd/generate.go b/cmd/generate.go index 845447e..e1a658e 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -1,21 +1,7 @@ package cmd import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "github.com/spf13/cobra" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - - "github.com/Skarlso/crd-to-sample-yaml/pkg" -) - -const ( - FormatHTML = "html" - FormatYAML = "yaml" ) type rootArgs struct { @@ -38,19 +24,14 @@ var ( generateCmd = &cobra.Command{ Use: "generate", Short: "Simply generate a CRD output.", - RunE: runGenerate, } args = &rootArgs{} ) -type Handler interface { - CRDs() ([]*v1beta1.CustomResourceDefinition, error) -} - func init() { rootCmd.AddCommand(generateCmd) - + // using persistent flags so all flags will be available for all sub commands. f := generateCmd.PersistentFlags() f.StringVarP(&args.fileLocation, "crd", "c", "", "The CRD file to generate a yaml from.") f.StringVarP(&args.folderLocation, "folder", "r", "", "A folder from which to parse a series of CRDs.") @@ -65,94 +46,3 @@ func init() { f.BoolVarP(&args.minimal, "minimal", "l", false, "If set, only the minimal required example yaml is generated.") f.BoolVar(&args.skipRandom, "no-random", false, "Skip generating random values that satisfy the property patterns.") } - -func runGenerate(_ *cobra.Command, _ []string) error { - crdHandler, err := constructHandler(args) - if err != nil { - return err - } - - if args.format == FormatHTML { - if err := pkg.LoadTemplates(); err != nil { - return fmt.Errorf("failed to load templates: %w", err) - } - } - - // determine location of output - if args.output == "" { - loc, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to determine executable location: %w", err) - } - - args.output = filepath.Dir(loc) - } - - crds, err := crdHandler.CRDs() - if err != nil { - return fmt.Errorf("failed to load CRDs: %w", err) - } - - var w io.WriteCloser - - if args.format == FormatHTML { - if args.stdOut { - w = os.Stdout - } else { - w, err = os.Create(args.output) - if err != nil { - return fmt.Errorf("failed to create output file: %w", err) - } - - defer w.Close() - } - - return pkg.RenderContent(w, crds, args.comments, args.minimal) - } - - var errs []error //nolint:prealloc // nope - for _, crd := range crds { - if args.stdOut { - w = os.Stdout - } else { - outputLocation := filepath.Join(args.output, crd.Name+"_sample."+args.format) - // closed later during render - outputFile, err := os.Create(outputLocation) - if err != nil { - errs = append(errs, fmt.Errorf("failed to create file at: '%s': %w", outputLocation, err)) - - continue - } - - w = outputFile - } - - errs = append(errs, pkg.Generate(crd, w, args.comments, args.minimal, args.skipRandom)) - } - - return errors.Join(errs...) -} - -func constructHandler(args *rootArgs) (Handler, error) { - var crdHandler Handler - - switch { - case args.fileLocation != "": - crdHandler = &FileHandler{location: args.fileLocation} - case args.folderLocation != "": - crdHandler = &FolderHandler{location: args.folderLocation} - case args.url != "": - crdHandler = &URLHandler{ - url: args.url, - username: args.username, - password: args.password, - token: args.token, - } - } - - if crdHandler == nil { - return nil, errors.New("one of the flags (file, folder, url) must be set") - } - - return crdHandler, nil -} diff --git a/cmd/schema.go b/cmd/schema.go new file mode 100644 index 0000000..96242c0 --- /dev/null +++ b/cmd/schema.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/json" +) + +// schemaCmd is a command that can generate json schemas. +var schemaCmd = &cobra.Command{ + Use: "schema", + Short: "Simply generate a JSON schema from the CRD.", + RunE: runGenerateSchema, +} + +func runGenerateSchema(_ *cobra.Command, _ []string) error { + crdHandler, err := constructHandler(args) + if err != nil { + return err + } + + // determine location of output + if args.output == "" { + loc, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine executable location: %w", err) + } + + args.output = filepath.Dir(loc) + } + + crds, err := crdHandler.CRDs() + if err != nil { + return fmt.Errorf("failed to load CRDs: %w", err) + } + + for _, crd := range crds { + for _, v := range crd.Spec.Versions { + if v.Schema.OpenAPIV3Schema.ID == "" { + v.Schema.OpenAPIV3Schema.ID = "https://crdtoyaml.com/" + crd.Spec.Names.Kind + "." + crd.Spec.Group + "." + v.Name + ".schema.json" + } + if v.Schema.OpenAPIV3Schema.Schema == "" { + v.Schema.OpenAPIV3Schema.Schema = "https://json-schema.org/draft/2020-12/schema" + } + content, err := json.Marshal(v.Schema.OpenAPIV3Schema) + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + + const perm = 0o600 + if err := os.WriteFile(filepath.Join(args.output, crd.Spec.Names.Kind+"."+crd.Spec.Group+"."+v.Name+".json"), content, perm); err != nil { + return fmt.Errorf("failed to write schema: %w", err) + } + } + } + + return nil +} + +func init() { + generateCmd.AddCommand(schemaCmd) +}