Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate an HTML based output for a given CRD when format is defined #66

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
181 changes: 181 additions & 0 deletions pkg/create_html_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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)
}

switch {
case len(properties[k].Properties) > 0 && properties[k].AdditionalProperties == nil:
requiredList = v.Required
out, err := parseCRD(properties[k].Properties, version, requiredList)
if err != nil {
return nil, err
}
p.Properties = out
case 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
case 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/[email protected]/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