Skip to content

Commit

Permalink
feat: add ability to load content from folders (#78)
Browse files Browse the repository at this point in the history
* feat: add ability to load content from folders

* added folder handler and fixed error wrapping

* added lint fixes
  • Loading branch information
Skarlso authored May 25, 2024
1 parent 4163089 commit c0d0732
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 60 deletions.
30 changes: 30 additions & 0 deletions cmd/file_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"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, fmt.Errorf("failed to unmarshal into custom resource definition: %w", err)
}

return []*v1beta1.CustomResourceDefinition{crd}, nil
}
59 changes: 59 additions & 0 deletions cmd/folder_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"fmt"
"io/fs"
"os"
"path/filepath"

"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/util/yaml"
)

type FolderHandler struct {
location string
}

func (h *FolderHandler) 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)
}

var crds []*v1beta1.CustomResourceDefinition

if err := filepath.Walk(h.location, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

if filepath.Ext(path) != ".yaml" {
fmt.Fprintln(os.Stderr, "skipping file "+path)

return nil
}

content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}

crd := &v1beta1.CustomResourceDefinition{}
if err := yaml.Unmarshal(content, crd); err != nil {
fmt.Fprintln(os.Stderr, "skipping none CRD file: "+path)

return nil //nolint:nilerr // intentional
}

crds = append(crds, crd)

return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk the selected folder: %w", err)
}

return crds, nil
}
139 changes: 85 additions & 54 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@ 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 (
FormatHTML = "html"
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{
Expand All @@ -29,76 +36,100 @@ 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 //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
}
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 != "":
crdHandler = &FolderHandler{location: 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
}
33 changes: 33 additions & 0 deletions cmd/url_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cmd

import (
"fmt"
"net/http"
"time"

"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) {
client := http.DefaultClient
client.Timeout = 10 * time.Second

f := fetcher.NewFetcher(client)
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, fmt.Errorf("failed to unmarshal into custom resource definition: %w", err)
}

return []*v1beta1.CustomResourceDefinition{crd}, nil
}
14 changes: 8 additions & 6 deletions pkg/create_html_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pkg
import (
"bytes"
"embed"
"errors"
"fmt"
"html/template"
"io"
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions pkg/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit c0d0732

Please sign in to comment.