Skip to content

Commit 501b9d0

Browse files
authored
feat: add config file for optional groupping (#134)
* feat: add config file for optional groupping * added sample that uses no groups
1 parent fd67dc3 commit 501b9d0

14 files changed

+289
-95
lines changed

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,31 @@ cty generate crd -r folder
107107

108108
Any other flag will work as before.
109109

110+
### Config File
111+
112+
It's possible to define a config file that designates groups for various rendered CRDs.
113+
114+
To use a config file, set the switch `--config`. A sample config file could look something like this:
115+
116+
```yaml
117+
apiGroups:
118+
- name: "com.aws.services"
119+
description: "Resources related to AWS services"
120+
files: # files and folders can be defined together or on their own
121+
- sample-crd/infrastructure.cluster.x-k8s.io_awsclusters.yaml
122+
- sample-crd/delivery.krok.app_krokcommands
123+
- name: "com.azure.services"
124+
description: "Resources related to Azure services"
125+
folders:
126+
- azure-crds
127+
```
128+
129+
![rendered with groups](imgs/parsed4_groups.png)
130+
131+
If no grouping information is provided, the rendered CRD's group version is used.
132+
133+
![rendered without groups](imgs/parsed4_groups_2.png)
134+
110135
## Schema Generation
111136

112137
`cty` also provides a way to generate a JSON Schema out of a CRD. Simply use:

cmd/config_handler.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"k8s.io/apimachinery/pkg/util/yaml"
8+
9+
"github.com/Skarlso/crd-to-sample-yaml/pkg"
10+
)
11+
12+
type ConfigHandler struct {
13+
configFileLocation string
14+
}
15+
16+
func (h *ConfigHandler) CRDs() ([]*pkg.SchemaType, error) {
17+
if _, err := os.Stat(h.configFileLocation); os.IsNotExist(err) {
18+
return nil, fmt.Errorf("file under '%s' does not exist", h.configFileLocation)
19+
}
20+
content, err := os.ReadFile(h.configFileLocation)
21+
if err != nil {
22+
return nil, fmt.Errorf("failed to read file: %w", err)
23+
}
24+
25+
configFile := &pkg.RenderConfig{}
26+
if err = yaml.Unmarshal(content, configFile); err != nil {
27+
return nil, fmt.Errorf("failed to unmarshal config file: %w", err)
28+
}
29+
30+
// for each api group, call file handler and folder handler and gather all
31+
// the CRDs.
32+
var result []*pkg.SchemaType
33+
34+
for _, group := range configFile.APIGroups {
35+
for _, file := range group.Files {
36+
handler := FileHandler{location: file, group: group.Name}
37+
fileResults, err := handler.CRDs()
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to process CRDs for files in groups %s: %w", group.Name, err)
40+
}
41+
42+
result = append(result, fileResults...)
43+
}
44+
45+
for _, folder := range group.Folders {
46+
handler := FolderHandler{location: folder, group: group.Name}
47+
folderResults, err := handler.CRDs()
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to process CRDs for folders %s: %w", handler.location, err)
50+
}
51+
52+
result = append(result, folderResults...)
53+
}
54+
}
55+
56+
return result, nil
57+
}

cmd/crd.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func runGenerate(_ *cobra.Command, _ []string) error {
5757
}
5858

5959
if crdArgs.format == FormatHTML {
60+
if crdArgs.output == "" {
61+
return errors.New("output must be set to a filename if format is HTML")
62+
}
63+
6064
if err := pkg.LoadTemplates(); err != nil {
6165
return fmt.Errorf("failed to load templates: %w", err)
6266
}
@@ -78,12 +82,6 @@ func runGenerate(_ *cobra.Command, _ []string) error {
7882
}
7983

8084
var w io.WriteCloser
81-
defer func() {
82-
if err := w.Close(); err != nil {
83-
_, _ = fmt.Fprintf(os.Stderr, "failed to close output file: %s", err.Error())
84-
}
85-
}()
86-
8785
if crdArgs.format == FormatHTML {
8886
if crdArgs.stdOut {
8987
w = os.Stdout
@@ -94,7 +92,13 @@ func runGenerate(_ *cobra.Command, _ []string) error {
9492
}
9593
}
9694

97-
return pkg.RenderContent(w, crds, crdArgs.comments, crdArgs.minimal, crdArgs.skipRandom)
95+
opts := pkg.RenderOpts{
96+
Comments: crdArgs.comments,
97+
Minimal: crdArgs.minimal,
98+
Random: crdArgs.skipRandom,
99+
}
100+
101+
return pkg.RenderContent(w, crds, opts)
98102
}
99103

100104
var errs []error //nolint:prealloc // nope
@@ -128,6 +132,8 @@ func constructHandler(args *rootArgs) (Handler, error) {
128132
crdHandler = &FileHandler{location: args.fileLocation}
129133
case args.folderLocation != "":
130134
crdHandler = &FolderHandler{location: args.folderLocation}
135+
case args.configFileLocation != "":
136+
crdHandler = &ConfigHandler{configFileLocation: args.configFileLocation}
131137
case args.url != "":
132138
crdHandler = &URLHandler{
133139
url: args.url,
@@ -138,7 +144,7 @@ func constructHandler(args *rootArgs) (Handler, error) {
138144
}
139145

140146
if crdHandler == nil {
141-
return nil, errors.New("one of the flags (file, folder, url) must be set")
147+
return nil, errors.New("one of the flags (file, folder, url, configFile) must be set")
142148
}
143149

144150
return crdHandler, nil

cmd/file_handler.go

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
type FileHandler struct {
1515
location string
16+
group string
1617
}
1718

1819
func (h *FileHandler) CRDs() ([]*pkg.SchemaType, error) {
@@ -43,5 +44,9 @@ func (h *FileHandler) CRDs() ([]*pkg.SchemaType, error) {
4344
return nil, nil
4445
}
4546

47+
if h.group != "" {
48+
schemaType.Rendering = pkg.Rendering{Group: h.group}
49+
}
50+
4651
return []*pkg.SchemaType{schemaType}, nil
4752
}

cmd/folder_handler.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
type FolderHandler struct {
1717
location string
18+
group string
1819
}
1920

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

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

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

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

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

6364
if schemaType != nil {
65+
if h.group != "" {
66+
schemaType.Rendering = pkg.Rendering{Group: h.group}
67+
}
68+
6469
crds = append(crds, schemaType)
6570
}
6671

cmd/generate.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
)
66

77
type rootArgs struct {
8-
fileLocation string
9-
folderLocation string
10-
url string
11-
username string
12-
password string
13-
token string
8+
fileLocation string
9+
folderLocation string
10+
configFileLocation string
11+
url string
12+
username string
13+
password string
14+
token string
1415
}
1516

1617
var (
@@ -33,4 +34,5 @@ func init() {
3334
f.StringVar(&args.username, "username", "", "Optional username to authenticate a URL.")
3435
f.StringVar(&args.password, "password", "", "Optional password to authenticate a URL.")
3536
f.StringVar(&args.token, "token", "", "A bearer token to authenticate a URL.")
37+
f.StringVar(&args.configFileLocation, "config", "", "An optional configuration file that can define grouping data for various rendered crds.")
3638
}

imgs/parsed4_groups.png

158 KB
Loading

imgs/parsed4_groups_2.png

219 KB
Loading

pkg/config_file_schema.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package pkg
2+
3+
// APIGroups defines groups by which grouping will happen in the resulting HTML output.
4+
type APIGroups struct {
5+
Name string `json:"name"`
6+
Description string `json:"description"`
7+
Files []string `json:"files,omitempty"`
8+
Folders []string `json:"folders,omitempty"`
9+
}
10+
11+
// RenderConfig defines a configuration for the resulting rendered HTML content.
12+
type RenderConfig struct {
13+
APIGroups []APIGroups `json:"apiGroups"`
14+
}

pkg/create_html_output.go

+75-27
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"html/template"
88
"io"
99
"io/fs"
10+
"os"
1011
"slices"
1112
"sort"
1213

@@ -64,47 +65,81 @@ func LoadTemplates() error {
6465
return nil
6566
}
6667

68+
// Group defines a single group with a list of rendered versions.
69+
type Group struct {
70+
Name string
71+
Page []ViewPage
72+
}
73+
74+
// GroupPage will have a list of groups and inside these groups
75+
// will be a list of page views.
76+
type GroupPage struct {
77+
Groups []Group
78+
}
79+
80+
type RenderOpts struct {
81+
Comments bool
82+
Minimal bool
83+
Random bool
84+
}
85+
6786
// RenderContent creates an HTML website from the CRD content.
68-
func RenderContent(w io.WriteCloser, crds []*SchemaType, comments, minimal, random bool) (err error) {
69-
allViews := make([]ViewPage, 0, len(crds))
87+
func RenderContent(w io.WriteCloser, crds []*SchemaType, opts RenderOpts) (err error) {
88+
defer func() {
89+
if err := w.Close(); err != nil {
90+
_, _ = fmt.Fprintf(os.Stderr, "failed to close output file: %s", err.Error())
91+
}
92+
}()
7093

71-
for _, crd := range crds {
72-
versions := make([]Version, 0)
73-
parser := NewParser(crd.Group, crd.Kind, comments, minimal, random)
94+
groups := buildUpGroup(crds)
7495

75-
for _, version := range crd.Versions {
76-
v, err := generate(version.Name, crd.Group, crd.Kind, version.Schema, minimal, parser)
77-
if err != nil {
78-
return fmt.Errorf("failed to generate yaml sample: %w", err)
96+
allGroups := make([]Group, 0)
97+
for name, group := range groups {
98+
allViews := make([]ViewPage, 0, len(group))
99+
100+
for _, crd := range group {
101+
versions := make([]Version, 0)
102+
parser := NewParser(crd.Group, crd.Kind, opts.Comments, opts.Minimal, opts.Random)
103+
104+
for _, version := range crd.Versions {
105+
v, err := generate(version.Name, crd.Group, crd.Kind, version.Schema, opts.Minimal, parser)
106+
if err != nil {
107+
return fmt.Errorf("failed to generate yaml sample: %w", err)
108+
}
109+
110+
versions = append(versions, v)
79111
}
80112

81-
versions = append(versions, v)
82-
}
113+
// parse validation instead
114+
if len(versions) == 0 && crd.Validation != nil {
115+
version, err := generate(crd.Validation.Name, crd.Group, crd.Kind, crd.Validation.Schema, opts.Minimal, parser)
116+
if err != nil {
117+
return fmt.Errorf("failed to generate yaml sample: %w", err)
118+
}
83119

84-
// parse validation instead
85-
if len(versions) == 0 && crd.Validation != nil {
86-
version, err := generate(crd.Validation.Name, crd.Group, crd.Kind, crd.Validation.Schema, minimal, parser)
87-
if err != nil {
88-
return fmt.Errorf("failed to generate yaml sample: %w", err)
120+
versions = append(versions, version)
121+
} else if len(versions) == 0 {
122+
continue
89123
}
90124

91-
versions = append(versions, version)
92-
} else if len(versions) == 0 {
93-
continue
94-
}
125+
view := ViewPage{
126+
Title: crd.Kind,
127+
Versions: versions,
128+
}
95129

96-
view := ViewPage{
97-
Title: crd.Kind,
98-
Versions: versions,
130+
allViews = append(allViews, view)
99131
}
100132

101-
allViews = append(allViews, view)
133+
allGroups = append(allGroups, Group{
134+
Name: name,
135+
Page: allViews,
136+
})
102137
}
103138

104-
t := templates["view.html"]
139+
t := templates["view_with_groups.html"]
105140

106-
index := Index{
107-
Page: allViews,
141+
index := GroupPage{
142+
Groups: allGroups,
108143
}
109144

110145
if err := t.Execute(w, index); err != nil {
@@ -114,6 +149,19 @@ func RenderContent(w io.WriteCloser, crds []*SchemaType, comments, minimal, rand
114149
return nil
115150
}
116151

152+
func buildUpGroup(crds []*SchemaType) map[string][]*SchemaType {
153+
result := map[string][]*SchemaType{}
154+
for _, crd := range crds {
155+
if crd.Rendering.Group == "" {
156+
crd.Rendering.Group = crd.Group
157+
}
158+
159+
result[crd.Rendering.Group] = append(result[crd.Rendering.Group], crd)
160+
}
161+
162+
return result
163+
}
164+
117165
func generate(name, group, kind string, properties *v1beta1.JSONSchemaProps, minimal bool, parser *Parser) (Version, error) {
118166
out, err := parseCRD(properties.Properties, name, minimal, group, kind, RootRequiredFields, 0)
119167
if err != nil {

0 commit comments

Comments
 (0)