Skip to content

Commit

Permalink
feat: generate an HTML based output for a given CRD when format is de…
Browse files Browse the repository at this point in the history
…fined
  • Loading branch information
Skarlso committed May 9, 2024
1 parent 77c203b commit c6fdfb5
Show file tree
Hide file tree
Showing 6 changed files with 7,694 additions and 93 deletions.
18 changes: 17 additions & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (
"github.com/Skarlso/crd-to-sample-yaml/pkg/fetcher"
)

const (
FormatHTML = "html"
FormatYAML = "yaml"
)

var (
// generateCmd is root for various `generate ...` commands.
generateCmd = &cobra.Command{
Expand All @@ -27,6 +32,7 @@ var (
fileLocation string
url string
output string
format string
stdOut bool
comments bool
)
Expand All @@ -38,6 +44,7 @@ func init() {
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")
}
Expand Down Expand Up @@ -68,19 +75,28 @@ func runGenerate(_ *cobra.Command, _ []string) error {
if err := yaml.Unmarshal(content, crd); err != nil {
return errors.New("failed to unmarshal into custom resource definition")
}

if stdOut {
w = os.Stdout
} else {
if output == "" {
output = filepath.Dir(fileLocation)
}
outputLocation := filepath.Join(output, crd.Name+"_sample.yaml")
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)
}
w = outputFile
}

if format == FormatHTML {
if err := pkg.LoadTemplates(); err != nil {
return fmt.Errorf("failed to load templates: %w", err)
}

return pkg.RenderContent(w, content, comments)
}

return pkg.Generate(crd, w, comments)
}
179 changes: 179 additions & 0 deletions pkg/create_html_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package pkg

import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"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.
type Version struct {
Version string
Kind string
Group string
Properties []*Property
Description string
YAML string
}

// ViewPage is the template for view.html.
type ViewPage struct {
Versions []Version
}

var (
//go:embed templates
files embed.FS
templates map[string]*template.Template
)

// LoadTemplates creates a map of loaded templates that are primed and ready to be rendered.
func LoadTemplates() error {
if templates == nil {
templates = make(map[string]*template.Template)
}
tmplFiles, err := fs.ReadDir(files, "templates")
if err != nil {
return err
}

for _, tmpl := range tmplFiles {
if tmpl.IsDir() {
continue
}
pt, err := template.ParseFS(files, "templates/"+tmpl.Name())
if err != nil {
return err
}

templates[tmpl.Name()] = pt
}

return nil
}

// RenderContent creates an HTML website from the CRD content.
func RenderContent(w io.Writer, crdContent []byte, comments 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)
}
versions := make([]Version, 0)
for _, version := range crd.Spec.Versions {
out, err := parseCRD(version.Schema.OpenAPIV3Schema.Properties, version.Name, version.Schema.OpenAPIV3Schema.Required)
if err != nil {
return fmt.Errorf("failed to parse properties: %w", err)
}
var buffer []byte
buf := bytes.NewBuffer(buffer)
if err := ParseProperties(crd.Spec.Group, version.Name, crd.Spec.Names.Kind, version.Schema.OpenAPIV3Schema.Properties, buf, 0, false, comments); err != nil {
return fmt.Errorf("failed to generate yaml sample: %w", err)
}
versions = append(versions, Version{
Version: version.Name,
Properties: out,
Kind: crd.Spec.Names.Kind,
Group: crd.Spec.Group,
Description: version.Schema.OpenAPIV3Schema.Description,
YAML: buf.String(),
})
}
view := ViewPage{
Versions: versions,
}
t := templates["view.html"]
if err := t.Execute(w, view); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

return nil
}

// Property builds up a Tree structure of embedded things.
type Property struct {
Name string
Description string
Type string
Nullable bool
Patterns string
Format string
Indent int
Version string
Default string
Required bool
Properties []*Property
}

// parseCRD takes the properties and constructs a linked list out of the embedded properties that the recursive
// template can call and construct linked divs.
func parseCRD(properties map[string]v1beta1.JSONSchemaProps, version string, requiredList []string) ([]*Property, error) {
output := make([]*Property, 0, len(properties))
sortedKeys := make([]string, 0, len(properties))

for k := range properties {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)

for _, k := range sortedKeys {
// Create the Property with the values necessary.
// Check if there are properties for it in Properties or in Array -> Properties.
// If yes, call parseCRD and add the result to the created properties Properties list.
// If not, or if we are done, add this new property to the list of properties and return it.
v := properties[k]
required := false
for _, item := range requiredList {
if item == k {
required = true

break
}
}
p := &Property{
Name: k,
Type: v.Type,
Description: v.Description,
Patterns: v.Pattern,
Format: v.Format,
Nullable: v.Nullable,
Version: version,
Required: required,
}
if v.Default != nil {
p.Default = string(v.Default.Raw)
}

if len(properties[k].Properties) > 0 && properties[k].AdditionalProperties == nil {

Check failure on line 153 in pkg/create_html_output.go

View workflow job for this annotation

GitHub Actions / run-test-suite

ifElseChain: rewrite if-else to switch statement (gocritic)
requiredList = v.Required
out, err := parseCRD(properties[k].Properties, version, requiredList)
if err != nil {
return nil, err
}
p.Properties = out
} else if properties[k].Type == array && properties[k].Items.Schema != nil && len(properties[k].Items.Schema.Properties) > 0 {
requiredList = v.Required
out, err := parseCRD(properties[k].Items.Schema.Properties, version, requiredList)
if err != nil {
return nil, err
}
p.Properties = out
} else if properties[k].AdditionalProperties != nil {
requiredList = v.Required
out, err := parseCRD(properties[k].AdditionalProperties.Schema.Properties, version, requiredList)
if err != nil {
return nil, err
}
p.Properties = out
}
output = append(output, p)
}

return output, nil
}
6 changes: 4 additions & 2 deletions pkg/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)

const array = "array"

// Generate takes a CRD content and path, and outputs.
func Generate(crd *v1beta1.CustomResourceDefinition, w io.WriteCloser, enableComments bool) (err error) {
defer func() {
Expand Down Expand Up @@ -87,7 +89,7 @@ func ParseProperties(group, version, kind string, properties map[string]v1beta1.
// If we are dealing with an array, and we have properties to parse
// we need to reparse all of them again.
var result string
if properties[k].Type == "array" && properties[k].Items.Schema != nil && len(properties[k].Items.Schema.Properties) > 0 {
if properties[k].Type == array && properties[k].Items.Schema != nil && len(properties[k].Items.Schema.Properties) > 0 {
w.write(file, fmt.Sprintf("\n%s- ", strings.Repeat(" ", indent)))
if err := ParseProperties(group, version, kind, properties[k].Items.Schema.Properties, file, indent+2, true, comments); err != nil {
return err
Expand Down Expand Up @@ -141,7 +143,7 @@ func outputValueType(v v1beta1.JSONSchemaProps) string {
return "true"
case "object":
return "{}"
case "array": // deal with arrays of other types that weren't objects
case array: // deal with arrays of other types that weren't objects
t := v.Items.Schema.Type
var s string
if t == st {
Expand Down
81 changes: 0 additions & 81 deletions pkg/templates/index.html

This file was deleted.

33 changes: 24 additions & 9 deletions pkg/templates/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,34 @@
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-twilight.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/halfmoon@1.1.1/css/halfmoon.min.css"
/>

<title>Preview CRDs</title>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="static/css/main.css" rel="stylesheet" type="text/css">
<link href="static/css/prism.css" rel="stylesheet" type="text/css">
<link href="static/css/prism-okaidia.css" rel="stylesheet" type="text/css">
<link href="static/css/root.css" rel="stylesheet" type="text/css">
<link href="static/css/halfmoon-variables.min.css" rel="stylesheet" type="text/css">
<style>
@media (max-width: 576px) {
body .content-wrapper > div {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}

body .content-wrapper {
padding-bottom: 2rem;
}
</style>
<style>

</style>
</head>

<body class="dark-mode" data-dm-shortcut-enabled="true" data-sidebar-shortcut-enabled="true">
Expand Down Expand Up @@ -88,10 +107,6 @@ <h1>
console.log("todo: loop through all elements and collapse them")
}
</script>
<script src="static/js/prism.js">
</script>
<script src="static/js/clipboard.min.js">
</script>
</body>
</html>

Expand Down
Loading

0 comments on commit c6fdfb5

Please sign in to comment.