diff --git a/cmd/crd.go b/cmd/crd.go new file mode 100644 index 0000000..0c3cecc --- /dev/null +++ b/cmd/crd.go @@ -0,0 +1,147 @@ +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 crdGenArgs struct { + comments bool + minimal bool + skipRandom bool + output string + format string + stdOut bool +} + +var crdArgs = &crdGenArgs{} + +type Handler interface { + CRDs() ([]*v1beta1.CustomResourceDefinition, error) +} + +func init() { + generateCmd.AddCommand(crdCmd) + f := crdCmd.PersistentFlags() + f.BoolVarP(&crdArgs.comments, "comments", "m", false, "If set, it will add descriptions as comments to each line where available.") + f.BoolVarP(&crdArgs.minimal, "minimal", "l", false, "If set, only the minimal required example yaml is generated.") + f.BoolVar(&crdArgs.skipRandom, "no-random", false, "Skip generating random values that satisfy the property patterns.") + f.StringVarP(&crdArgs.output, "output", "o", "", "The location of the output file. Default is next to the CRD.") + f.StringVarP(&crdArgs.format, "format", "f", FormatYAML, "The format in which to output. Default is YAML. Options are: yaml, html.") + f.BoolVarP(&crdArgs.stdOut, "stdout", "s", false, "If set, it will output the generated content to stdout.") +} + +func runGenerate(_ *cobra.Command, _ []string) error { + crdHandler, err := constructHandler(args) + if err != nil { + return err + } + + if crdArgs.format == FormatHTML { + if err := pkg.LoadTemplates(); err != nil { + return fmt.Errorf("failed to load templates: %w", err) + } + } + + // determine location of output + if crdArgs.output == "" { + loc, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine executable location: %w", err) + } + + crdArgs.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 crdArgs.format == FormatHTML { + if crdArgs.stdOut { + w = os.Stdout + } else { + w, err = os.Create(crdArgs.output) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + + defer func() { + if err := w.Close(); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to close output file: %s", err.Error()) + } + }() + } + + return pkg.RenderContent(w, crds, crdArgs.comments, crdArgs.minimal) + } + + var errs []error //nolint:prealloc // nope + for _, crd := range crds { + if crdArgs.stdOut { + w = os.Stdout + } else { + outputLocation := filepath.Join(crdArgs.output, crd.Name+"_sample."+crdArgs.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, crdArgs.comments, crdArgs.minimal, crdArgs.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..d9d0a1f 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 { @@ -25,12 +11,6 @@ type rootArgs struct { username string password string token string - output string - format string - stdOut bool - comments bool - minimal bool - skipRandom bool } var ( @@ -38,19 +18,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.") @@ -58,101 +33,4 @@ func init() { f.StringVar(&args.username, "username", "", "Optional username to authenticate a URL.") f.StringVar(&args.password, "password", "", "Optional password to authenticate a URL.") f.StringVar(&args.token, "token", "", "A bearer token to authenticate a URL.") - f.StringVarP(&args.output, "output", "o", "", "The location of the output file. Default is next to the CRD.") - f.StringVarP(&args.format, "format", "f", FormatYAML, "The format in which to output. Default is YAML. Options are: yaml, html.") - f.BoolVarP(&args.stdOut, "stdout", "s", false, "If set, it will output the generated content to stdout.") - f.BoolVarP(&args.comments, "comments", "m", false, "If set, it will add descriptions as comments to each line where available.") - 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..c833743 --- /dev/null +++ b/cmd/schema.go @@ -0,0 +1,73 @@ +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, +} + +type schemaCmdArgs struct { + outputFolder string +} + +var schemaArgs = &schemaCmdArgs{} + +func init() { + generateCmd.AddCommand(schemaCmd) + f := schemaCmd.PersistentFlags() + f.StringVarP(&schemaArgs.outputFolder, "output", "o", ".", "output location of the generated schema files") +} + +func runGenerateSchema(_ *cobra.Command, _ []string) error { + crdHandler, err := constructHandler(args) + if err != nil { + return err + } + + // determine location of output + if schemaArgs.outputFolder == "" { + loc, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine executable location: %w", err) + } + + schemaArgs.outputFolder = 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(schemaArgs.outputFolder, crd.Spec.Names.Kind+"."+crd.Spec.Group+"."+v.Name+".schema.json"), content, perm); err != nil { + return fmt.Errorf("failed to write schema: %w", err) + } + } + } + + return nil +}