Skip to content

Commit

Permalink
feat: add json schema generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Skarlso committed Oct 12, 2024
1 parent b288180 commit bd403dd
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 111 deletions.
125 changes: 125 additions & 0 deletions cmd/crd.go
Original file line number Diff line number Diff line change
@@ -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
}
112 changes: 1 addition & 111 deletions cmd/generate.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.")
Expand All @@ -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
}
64 changes: 64 additions & 0 deletions cmd/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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(cmd *cobra.Command, strings []string) error {

Check failure on line 19 in cmd/schema.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'cmd' seems to be unused, consider removing or renaming it as _ (revive)
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)
}

if err := os.WriteFile(filepath.Join(args.output, crd.Spec.Names.Kind+"."+crd.Spec.Group+"."+v.Name+".json"), content, 0o644); err != nil {

Check failure on line 53 in cmd/schema.go

View workflow job for this annotation

GitHub Actions / lint

G306: Expect WriteFile permissions to be 0600 or less (gosec)
return fmt.Errorf("failed to write schema: %w", err)
}
}
}

return nil
}

func init() {
generateCmd.AddCommand(schemaCmd)
}

0 comments on commit bd403dd

Please sign in to comment.