Skip to content

Commit

Permalink
feat: add config file for optional groupping
Browse files Browse the repository at this point in the history
  • Loading branch information
Skarlso committed Oct 27, 2024
1 parent fd67dc3 commit fb4424a
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 95 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,27 @@ cty generate crd -r folder

Any other flag will work as before.

### Config File

It's possible to define a config file that designates groups for various rendered CRDs.

To use a config file, set the switch `--config`. A sample config file could look something like this:

```yaml
apiGroups:
- name: "com.aws.services"
description: "Resources related to AWS services"
files: # files and folders can be defined together or on their own
- sample-crd/infrastructure.cluster.x-k8s.io_awsclusters.yaml
- sample-crd/delivery.krok.app_krokcommands
- name: "com.azure.services"
description: "Resources related to Azure services"
folders:
- azure-crds
```

![rendered with groups](imgs/parsed4_groups.png)

## Schema Generation

`cty` also provides a way to generate a JSON Schema out of a CRD. Simply use:
Expand Down
57 changes: 57 additions & 0 deletions cmd/config_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cmd

import (
"fmt"
"os"

"k8s.io/apimachinery/pkg/util/yaml"

"github.com/Skarlso/crd-to-sample-yaml/pkg"
)

type ConfigHandler struct {
configFileLocation string
}

func (h *ConfigHandler) CRDs() ([]*pkg.SchemaType, error) {
if _, err := os.Stat(h.configFileLocation); os.IsNotExist(err) {
return nil, fmt.Errorf("file under '%s' does not exist", h.configFileLocation)
}
content, err := os.ReadFile(h.configFileLocation)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

configFile := &pkg.RenderConfig{}
if err = yaml.Unmarshal(content, configFile); err != nil {
return nil, fmt.Errorf("failed to unmarshal config file: %w", err)
}

// for each api group, call file handler and folder handler and gather all
// the CRDs.
var result []*pkg.SchemaType

for _, group := range configFile.ApiGroups {
for _, file := range group.Files {
handler := FileHandler{location: file, group: group.Name}
fileResults, err := handler.CRDs()
if err != nil {
return nil, fmt.Errorf("failed to process CRDs for files in groups %s: %w", group.Name, err)
}

result = append(result, fileResults...)
}

for _, folder := range group.Folders {
handler := FolderHandler{location: folder, group: group.Name}
folderResults, err := handler.CRDs()
if err != nil {
return nil, fmt.Errorf("failed to process CRDs for folders %s: %w", handler.location, err)
}

result = append(result, folderResults...)
}
}

return result, nil
}
22 changes: 14 additions & 8 deletions cmd/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func runGenerate(_ *cobra.Command, _ []string) error {
}

if crdArgs.format == FormatHTML {
if crdArgs.output == "" {
return fmt.Errorf("output must be set to a filename if format is HTML")

Check failure on line 61 in cmd/crd.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Errorf can be replaced with errors.New (perfsprint)
}

if err := pkg.LoadTemplates(); err != nil {
return fmt.Errorf("failed to load templates: %w", err)
}
Expand All @@ -78,12 +82,6 @@ func runGenerate(_ *cobra.Command, _ []string) error {
}

var w io.WriteCloser
defer func() {
if err := w.Close(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to close output file: %s", err.Error())
}
}()

if crdArgs.format == FormatHTML {
if crdArgs.stdOut {
w = os.Stdout
Expand All @@ -94,7 +92,13 @@ func runGenerate(_ *cobra.Command, _ []string) error {
}
}

return pkg.RenderContent(w, crds, crdArgs.comments, crdArgs.minimal, crdArgs.skipRandom)
opts := pkg.RenderOpts{
Comments: crdArgs.comments,
Minimal: crdArgs.minimal,
Random: crdArgs.skipRandom,
}

return pkg.RenderContent(w, crds, opts)
}

var errs []error //nolint:prealloc // nope
Expand Down Expand Up @@ -128,6 +132,8 @@ func constructHandler(args *rootArgs) (Handler, error) {
crdHandler = &FileHandler{location: args.fileLocation}
case args.folderLocation != "":
crdHandler = &FolderHandler{location: args.folderLocation}
case args.configFileLocation != "":
crdHandler = &ConfigHandler{configFileLocation: args.configFileLocation}
case args.url != "":
crdHandler = &URLHandler{
url: args.url,
Expand All @@ -138,7 +144,7 @@ func constructHandler(args *rootArgs) (Handler, error) {
}

if crdHandler == nil {
return nil, errors.New("one of the flags (file, folder, url) must be set")
return nil, errors.New("one of the flags (file, folder, url, configFile) must be set")
}

return crdHandler, nil
Expand Down
5 changes: 5 additions & 0 deletions cmd/file_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type FileHandler struct {
location string
group string
}

func (h *FileHandler) CRDs() ([]*pkg.SchemaType, error) {
Expand Down Expand Up @@ -43,5 +44,9 @@ func (h *FileHandler) CRDs() ([]*pkg.SchemaType, error) {
return nil, nil
}

if h.group != "" {
schemaType.Rendering = pkg.Rendering{Group: h.group}
}

return []*pkg.SchemaType{schemaType}, nil
}
9 changes: 7 additions & 2 deletions cmd/folder_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

type FolderHandler struct {
location string
group string
}

func (h *FolderHandler) CRDs() ([]*pkg.SchemaType, error) {
Expand All @@ -34,7 +35,7 @@ func (h *FolderHandler) CRDs() ([]*pkg.SchemaType, error) {
}

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

return nil
}
Expand All @@ -51,7 +52,7 @@ func (h *FolderHandler) CRDs() ([]*pkg.SchemaType, error) {

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

return nil //nolint:nilerr // intentional
}
Expand All @@ -61,6 +62,10 @@ func (h *FolderHandler) CRDs() ([]*pkg.SchemaType, error) {
}

if schemaType != nil {
if h.group != "" {
schemaType.Rendering = pkg.Rendering{Group: h.group}
}

crds = append(crds, schemaType)
}

Expand Down
14 changes: 8 additions & 6 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
)

type rootArgs struct {
fileLocation string
folderLocation string
url string
username string
password string
token string
fileLocation string
folderLocation string
configFileLocation string
url string
username string
password string
token string
}

var (
Expand All @@ -33,4 +34,5 @@ func init() {
f.StringVar(&args.username, "username", "", "Optional username to authenticate a URL.")
f.StringVar(&args.password, "password", "", "Optional password to authenticate a URL.")
f.StringVar(&args.token, "token", "", "A bearer token to authenticate a URL.")
f.StringVar(&args.configFileLocation, "config", "", "An optional configuration file that can define grouping data for various rendered crds.")
}
Binary file added imgs/parsed4_groups.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions pkg/config_file_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package pkg

// ApiGroups defines groups by which grouping will happen in the resulting HTML output.
type ApiGroups struct {

Check failure on line 4 in pkg/config_file_schema.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: type ApiGroups should be APIGroups (revive)
Name string `json:"name"`
Description string `json:"description"`
Files []string `json:"files,omitempty"`
Folders []string `json:"folders,omitempty"`
}

// RenderConfig defines a configuration for the resulting rendered HTML content.
type RenderConfig struct {
ApiGroups []ApiGroups `json:"apiGroups"`

Check failure on line 13 in pkg/config_file_schema.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field ApiGroups should be APIGroups (revive)
}
105 changes: 78 additions & 27 deletions pkg/create_html_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import (
"html/template"
"io"
"io/fs"
"os"
"slices"
"sort"

"github.com/Skarlso/crd-to-sample-yaml/v1beta1"
)

const defaultGroup = "Ungrouped"

Check failure on line 17 in pkg/create_html_output.go

View workflow job for this annotation

GitHub Actions / lint

const `defaultGroup` is unused (unused)

type Index struct {
Page []ViewPage
}
Expand Down Expand Up @@ -64,47 +67,81 @@ func LoadTemplates() error {
return nil
}

// Group defines a single group with a list of rendered versions.
type Group struct {
Name string
Page []ViewPage
}

// GroupPage will have a list of groups and inside these groups
// will be a list of page views.
type GroupPage struct {
Groups []Group
}

type RenderOpts struct {
Comments bool
Minimal bool
Random bool
}

// RenderContent creates an HTML website from the CRD content.
func RenderContent(w io.WriteCloser, crds []*SchemaType, comments, minimal, random bool) (err error) {
allViews := make([]ViewPage, 0, len(crds))
func RenderContent(w io.WriteCloser, crds []*SchemaType, opts RenderOpts) (err error) {
defer func() {
if err := w.Close(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to close output file: %s", err.Error())
}
}()

for _, crd := range crds {
versions := make([]Version, 0)
parser := NewParser(crd.Group, crd.Kind, comments, minimal, random)
groups := buildUpGroup(crds)

for _, version := range crd.Versions {
v, err := generate(version.Name, crd.Group, crd.Kind, version.Schema, minimal, parser)
if err != nil {
return fmt.Errorf("failed to generate yaml sample: %w", err)
allGroups := make([]Group, 0)
for name, group := range groups {
allViews := make([]ViewPage, 0, len(group))

for _, crd := range group {
versions := make([]Version, 0)
parser := NewParser(crd.Group, crd.Kind, opts.Comments, opts.Minimal, opts.Random)

for _, version := range crd.Versions {
v, err := generate(version.Name, crd.Group, crd.Kind, version.Schema, opts.Minimal, parser)
if err != nil {
return fmt.Errorf("failed to generate yaml sample: %w", err)
}

versions = append(versions, v)
}

versions = append(versions, v)
}
// parse validation instead
if len(versions) == 0 && crd.Validation != nil {
version, err := generate(crd.Validation.Name, crd.Group, crd.Kind, crd.Validation.Schema, opts.Minimal, parser)
if err != nil {
return fmt.Errorf("failed to generate yaml sample: %w", err)
}

// parse validation instead
if len(versions) == 0 && crd.Validation != nil {
version, err := generate(crd.Validation.Name, crd.Group, crd.Kind, crd.Validation.Schema, minimal, parser)
if err != nil {
return fmt.Errorf("failed to generate yaml sample: %w", err)
versions = append(versions, version)
} else if len(versions) == 0 {
continue
}

versions = append(versions, version)
} else if len(versions) == 0 {
continue
}
view := ViewPage{
Title: crd.Kind,
Versions: versions,
}

view := ViewPage{
Title: crd.Kind,
Versions: versions,
allViews = append(allViews, view)
}

allViews = append(allViews, view)
allGroups = append(allGroups, Group{
Name: name,
Page: allViews,
})
}

t := templates["view.html"]
t := templates["view_with_groups.html"]

index := Index{
Page: allViews,
index := GroupPage{
Groups: allGroups,
}

if err := t.Execute(w, index); err != nil {
Expand All @@ -114,6 +151,20 @@ func RenderContent(w io.WriteCloser, crds []*SchemaType, comments, minimal, rand
return nil
}

func buildUpGroup(crds []*SchemaType) map[string][]*SchemaType {
var result = make(map[string][]*SchemaType)
for _, crd := range crds {
// TODO: If empty, it should be the api version
if crd.Rendering.Group == "" {
crd.Rendering.Group = crd.Group
}

result[crd.Rendering.Group] = append(result[crd.Rendering.Group], crd)
}

return result
}

func generate(name, group, kind string, properties *v1beta1.JSONSchemaProps, minimal bool, parser *Parser) (Version, error) {
out, err := parseCRD(properties.Properties, name, minimal, group, kind, RootRequiredFields, 0)
if err != nil {
Expand Down
Loading

0 comments on commit fb4424a

Please sign in to comment.