From ca9e4827f2f0396aec7f55dbf5eadd2a6a4191e3 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Sat, 25 May 2024 11:42:40 +0200 Subject: [PATCH] feat: add ability to load content from folders --- cmd/file_handler.go | 31 +++++++++ cmd/folder_handler.go | 1 + cmd/generate.go | 138 +++++++++++++++++++++++--------------- cmd/url_handler.go | 30 +++++++++ pkg/create_html_output.go | 14 ++-- pkg/generate.go | 1 + 6 files changed, 155 insertions(+), 60 deletions(-) create mode 100644 cmd/file_handler.go create mode 100644 cmd/folder_handler.go create mode 100644 cmd/url_handler.go diff --git a/cmd/file_handler.go b/cmd/file_handler.go new file mode 100644 index 0000000..83ff900 --- /dev/null +++ b/cmd/file_handler.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/util/yaml" +) + +type FileHandler struct { + location string +} + +func (h *FileHandler) CRDs() ([]*v1beta1.CustomResourceDefinition, error) { + if _, err := os.Stat(h.location); os.IsNotExist(err) { + return nil, fmt.Errorf("file under '%s' does not exist", h.location) + } + content, err := os.ReadFile(h.location) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + crd := &v1beta1.CustomResourceDefinition{} + if err := yaml.Unmarshal(content, crd); err != nil { + return nil, errors.New("failed to unmarshal into custom resource definition") + } + + return []*v1beta1.CustomResourceDefinition{crd}, nil +} diff --git a/cmd/folder_handler.go b/cmd/folder_handler.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cmd/folder_handler.go @@ -0,0 +1 @@ +package cmd diff --git a/cmd/generate.go b/cmd/generate.go index 1f3fe4e..f8d39ba 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -4,16 +4,12 @@ import ( "errors" "fmt" "io" - "net/http" "os" "path/filepath" + "github.com/Skarlso/crd-to-sample-yaml/pkg" "github.com/spf13/cobra" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apimachinery/pkg/util/yaml" - - "github.com/Skarlso/crd-to-sample-yaml/pkg" - "github.com/Skarlso/crd-to-sample-yaml/pkg/fetcher" ) const ( @@ -21,6 +17,17 @@ const ( FormatYAML = "yaml" ) +type rootArgs struct { + fileLocation string + folderLocation string + url string + output string + format string + stdOut bool + comments bool + minimal bool +} + var ( // generateCmd is root for various `generate ...` commands. generateCmd = &cobra.Command{ @@ -29,76 +36,99 @@ var ( RunE: runGenerate, } - fileLocation string - url string - output string - format string - stdOut bool - comments bool - minimal bool + args = &rootArgs{} ) +type Handler interface { + CRDs() ([]*v1beta1.CustomResourceDefinition, error) +} + func init() { rootCmd.AddCommand(generateCmd) f := generateCmd.PersistentFlags() - f.StringVarP(&fileLocation, "crd", "c", "", "The CRD file to generate a yaml from.") - f.StringVarP(&url, "url", "u", "", "If provided, will use this URL to fetch CRD YAML content from.") - f.StringVarP(&output, "output", "o", "", "The location of the output file. Default is next to the CRD.") - f.StringVarP(&format, "format", "f", FormatYAML, "The format in which to output. Default is YAML. Options are: yaml, html.") - f.BoolVarP(&stdOut, "stdout", "s", false, "If set, it will output the generated content to stdout.") - f.BoolVarP(&comments, "comments", "m", false, "If set, it will add descriptions as comments to each line where available.") - f.BoolVarP(&minimal, "minimal", "l", false, "If set, only the minimal required example yaml is generated.") + 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.") + f.StringVarP(&args.url, "url", "u", "", "If provided, will use this URL to fetch CRD YAML content from.") + 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.") } func runGenerate(_ *cobra.Command, _ []string) error { - var ( - content []byte - err error - w io.WriteCloser - ) - if url != "" { - f := fetcher.NewFetcher(http.DefaultClient) - content, err = f.Fetch(url) - if err != nil { - return fmt.Errorf("failed to fetch content: %w", err) - } - } else { - if _, err := os.Stat(fileLocation); os.IsNotExist(err) { - return fmt.Errorf("file under '%s' does not exist", fileLocation) + 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) } - content, err = os.ReadFile(fileLocation) + } + + // determine location of output + if args.output == "" { + loc, err := os.Executable() if err != nil { - return fmt.Errorf("failed to read file: %w", err) + return fmt.Errorf("failed to determine executable location: %w", err) } + + args.output = filepath.Dir(loc) } - crd := &v1beta1.CustomResourceDefinition{} - if err := yaml.Unmarshal(content, crd); err != nil { - return errors.New("failed to unmarshal into custom resource definition") + crds, err := crdHandler.CRDs() + if err != nil { + return fmt.Errorf("failed to load CRDs: %w", err) } - if stdOut { - w = os.Stdout - } else { - if output == "" { - output = filepath.Dir(fileLocation) + var w io.WriteCloser + + var errs []error + 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 } - outputLocation := filepath.Join(output, crd.Name+"_sample."+format) - outputFile, err := os.Create(outputLocation) - if err != nil { - return fmt.Errorf("failed to create file at: '%s': %w", outputLocation, err) + + if args.format == FormatHTML { + errs = append(errs, pkg.RenderContent(w, crd, args.comments, args.minimal)) + + continue } - w = outputFile + + errs = append(errs, pkg.Generate(crd, w, args.comments, args.minimal)) } - if format == FormatHTML { - if err := pkg.LoadTemplates(); err != nil { - return fmt.Errorf("failed to load templates: %w", err) - } + 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 != "": + case args.url != "": + crdHandler = &URLHandler{url: args.url} + } - return pkg.RenderContent(w, content, comments, minimal) + if crdHandler == nil { + return nil, errors.New("one of the flags (file, folder, url) must be set") } - return pkg.Generate(crd, w, comments, minimal) + return crdHandler, nil } diff --git a/cmd/url_handler.go b/cmd/url_handler.go new file mode 100644 index 0000000..642b7b7 --- /dev/null +++ b/cmd/url_handler.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "errors" + "fmt" + "net/http" + + "github.com/Skarlso/crd-to-sample-yaml/pkg/fetcher" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/util/yaml" +) + +type URLHandler struct { + url string +} + +func (h *URLHandler) CRDs() ([]*v1beta1.CustomResourceDefinition, error) { + f := fetcher.NewFetcher(http.DefaultClient) + content, err := f.Fetch(h.url) + if err != nil { + return nil, fmt.Errorf("failed to fetch content: %w", err) + } + + crd := &v1beta1.CustomResourceDefinition{} + if err := yaml.Unmarshal(content, crd); err != nil { + return nil, errors.New("failed to unmarshal into custom resource definition") + } + + return []*v1beta1.CustomResourceDefinition{crd}, nil +} diff --git a/pkg/create_html_output.go b/pkg/create_html_output.go index 1e725ae..3c987b4 100644 --- a/pkg/create_html_output.go +++ b/pkg/create_html_output.go @@ -3,6 +3,7 @@ package pkg import ( "bytes" "embed" + "errors" "fmt" "html/template" "io" @@ -11,7 +12,6 @@ import ( "sort" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apimachinery/pkg/util/yaml" ) // Version wraps a top level version resource which contains the underlying openAPIV3Schema. @@ -61,11 +61,13 @@ func LoadTemplates() error { } // RenderContent creates an HTML website from the CRD content. -func RenderContent(w io.Writer, crdContent []byte, comments, minimal bool) error { - crd := &v1beta1.CustomResourceDefinition{} - if err := yaml.Unmarshal(crdContent, crd); err != nil { - return fmt.Errorf("failed to unmarshal into custom resource definition: %w", err) - } +func RenderContent(w io.WriteCloser, crd *v1beta1.CustomResourceDefinition, comments, minimal bool) (err error) { + defer func() { + if cerr := w.Close(); cerr != nil { + err = errors.Join(err, cerr) + } + }() + versions := make([]Version, 0) parser := NewParser(crd.Spec.Group, crd.Spec.Names.Kind, comments, minimal) diff --git a/pkg/generate.go b/pkg/generate.go index 697e8d6..953640c 100644 --- a/pkg/generate.go +++ b/pkg/generate.go @@ -22,6 +22,7 @@ func Generate(crd *v1beta1.CustomResourceDefinition, w io.WriteCloser, enableCom err = errors.Join(err, cerr) } }() + parser := NewParser(crd.Spec.Group, crd.Spec.Names.Kind, enableComments, minimal) for i, version := range crd.Spec.Versions { if err := parser.ParseProperties(version.Name, w, version.Schema.OpenAPIV3Schema.Properties, RootRequiredFields); err != nil {