Skip to content

Commit ae28ffc

Browse files
committed
feat: add new golang script generatig examples doc on website
1 parent 53f94a6 commit ae28ffc

File tree

5 files changed

+510
-6
lines changed

5 files changed

+510
-6
lines changed

examples/aws/s3bucket/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# S3 Buckets
2+
3+
This example demonstrates how to create and manage AWS S3 buckets using kro's ResourceGraphDefinition.
4+
5+
## Overview
6+
7+
The S3 bucket example shows how to define an S3 bucket resource with common configurations such as versioning, encryption, and access policies. This is useful for applications that need object storage with consistent configuration across environments.

scripts/generate-examples-doc.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//go:build ignore
2+
// +build ignore
3+
4+
package main
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"sort"
12+
"strings"
13+
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
const (
18+
configFile = "website/examples.config.yaml"
19+
outputBaseDir = "website/docs/examples"
20+
)
21+
22+
// ExampleEntry represents a single example entry in the config
23+
type ExampleEntry struct {
24+
Name string `yaml:"name"`
25+
Description string `yaml:"description"`
26+
SidebarPosition int `yaml:"sidebar_position"`
27+
Files []FilePath `yaml:"files"`
28+
}
29+
30+
// FilePath represents a path entry in the files list
31+
type FilePath struct {
32+
Path string `yaml:"path"`
33+
}
34+
35+
// ExamplesConfig represents the entire configuration file
36+
// The key is the category name (e.g., "kubernetes", "aws")
37+
type ExamplesConfig map[string][]ExampleEntry
38+
39+
func main() {
40+
if err := run(); err != nil {
41+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
42+
os.Exit(1)
43+
}
44+
}
45+
46+
func run() error {
47+
// Read the config file
48+
config, err := readConfig(configFile)
49+
if err != nil {
50+
return fmt.Errorf("failed to read config file: %w", err)
51+
}
52+
53+
// Process each category
54+
for category, examples := range config {
55+
fmt.Printf("Processing category: %s\n", category)
56+
57+
for idx, example := range examples {
58+
sidebarPosition := example.SidebarPosition
59+
if sidebarPosition == 0 {
60+
sidebarPosition = idx + 1
61+
}
62+
63+
for _, file := range example.Files {
64+
// Resolve the path relative to the config file location
65+
examplePath := filepath.Join(filepath.Dir(configFile), file.Path)
66+
67+
if err := processExample(category, example, examplePath, sidebarPosition); err != nil {
68+
return fmt.Errorf("failed to process example %s: %w", example.Name, err)
69+
}
70+
}
71+
}
72+
}
73+
74+
fmt.Println("Documentation generation completed successfully!")
75+
return nil
76+
}
77+
78+
func readConfig(path string) (ExamplesConfig, error) {
79+
data, err := os.ReadFile(path)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
82+
}
83+
84+
var config ExamplesConfig
85+
if err := yaml.Unmarshal(data, &config); err != nil {
86+
return nil, fmt.Errorf("failed to parse YAML: %w", err)
87+
}
88+
89+
return config, nil
90+
}
91+
92+
func processExample(category string, example ExampleEntry, examplePath string, sidebarPosition int) error {
93+
fmt.Printf(" Processing example: %s (path: %s)\n", example.Name, examplePath)
94+
95+
// Check if the example directory exists
96+
info, err := os.Stat(examplePath)
97+
if err != nil {
98+
return fmt.Errorf("example path does not exist: %s: %w", examplePath, err)
99+
}
100+
if !info.IsDir() {
101+
return fmt.Errorf("example path is not a directory: %s", examplePath)
102+
}
103+
104+
// Read README.md
105+
readmePath := filepath.Join(examplePath, "README.md")
106+
readmeContent, err := os.ReadFile(readmePath)
107+
if err != nil {
108+
return fmt.Errorf("failed to read README.md at %s: %w", readmePath, err)
109+
}
110+
111+
// Collect manifest files
112+
manifestFiles, err := collectManifestFiles(examplePath)
113+
if err != nil {
114+
return fmt.Errorf("failed to collect manifest files: %w", err)
115+
}
116+
117+
// Generate markdown content
118+
markdownContent := generateMarkdown(string(readmeContent), manifestFiles, sidebarPosition)
119+
120+
// Determine output path
121+
outputFileName := toKebabCase(example.Name) + ".md"
122+
outputDir := filepath.Join(outputBaseDir, category)
123+
outputPath := filepath.Join(outputDir, outputFileName)
124+
125+
// Ensure output directory exists
126+
if err := os.MkdirAll(outputDir, 0755); err != nil {
127+
return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
128+
}
129+
130+
// Write the markdown file
131+
if err := os.WriteFile(outputPath, []byte(markdownContent), 0644); err != nil {
132+
return fmt.Errorf("failed to write output file %s: %w", outputPath, err)
133+
}
134+
135+
fmt.Printf(" Generated: %s\n", outputPath)
136+
return nil
137+
}
138+
139+
// ManifestFile represents a manifest file with its name and content
140+
type ManifestFile struct {
141+
Name string
142+
Content string
143+
}
144+
145+
func collectManifestFiles(dirPath string) ([]ManifestFile, error) {
146+
var manifestFiles []ManifestFile
147+
148+
entries, err := os.ReadDir(dirPath)
149+
if err != nil {
150+
return nil, fmt.Errorf("failed to read directory %s: %w", dirPath, err)
151+
}
152+
153+
for _, entry := range entries {
154+
if entry.IsDir() {
155+
continue
156+
}
157+
158+
name := entry.Name()
159+
if strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") {
160+
filePath := filepath.Join(dirPath, name)
161+
content, err := os.ReadFile(filePath)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
164+
}
165+
166+
manifestFiles = append(manifestFiles, ManifestFile{
167+
Name: name,
168+
Content: string(content),
169+
})
170+
}
171+
}
172+
173+
// Sort by filename for consistent output
174+
sort.Slice(manifestFiles, func(i, j int) bool {
175+
return manifestFiles[i].Name < manifestFiles[j].Name
176+
})
177+
178+
return manifestFiles, nil
179+
}
180+
181+
func generateMarkdown(readmeContent string, manifestFiles []ManifestFile, sidebarPosition int) string {
182+
var sb strings.Builder
183+
184+
// Add frontmatter
185+
sb.WriteString("---\n")
186+
sb.WriteString(fmt.Sprintf("sidebar_position: %d\n", sidebarPosition))
187+
sb.WriteString("---\n\n")
188+
189+
// Add Tabs import for Docusaurus
190+
if len(manifestFiles) > 0 {
191+
sb.WriteString("import Tabs from '@theme/Tabs';\n")
192+
sb.WriteString("import TabItem from '@theme/TabItem';\n\n")
193+
}
194+
195+
// Add README content
196+
sb.WriteString(readmeContent)
197+
198+
// Add Manifest files in Tabs component
199+
if len(manifestFiles) > 0 {
200+
// Add Tab
201+
sb.WriteString("## Manifest files\n\n")
202+
sb.WriteString("\n\n<Tabs>\n")
203+
204+
for _, manifestFile := range manifestFiles {
205+
// Generate a value identifier from filename (remove extension and special chars)
206+
valueID := strings.TrimSuffix(manifestFile.Name, filepath.Ext(manifestFile.Name))
207+
valueID = strings.ReplaceAll(valueID, ".", "-")
208+
209+
sb.WriteString(fmt.Sprintf(" <TabItem value=\"%s\" label=\"%s\">\n\n", valueID, manifestFile.Name))
210+
sb.WriteString("```kro\n")
211+
sb.WriteString(manifestFile.Content)
212+
if !strings.HasSuffix(manifestFile.Content, "\n") {
213+
sb.WriteString("\n")
214+
}
215+
sb.WriteString("```\n\n")
216+
sb.WriteString(" </TabItem>\n")
217+
}
218+
219+
sb.WriteString("</Tabs>\n")
220+
}
221+
222+
return sb.String()
223+
}
224+
225+
// toKebabCase converts a string to kebab-case
226+
// e.g., "SaaS Multi Tenant" -> "saas-multi-tenant"
227+
func toKebabCase(s string) string {
228+
// Convert to lowercase
229+
s = strings.ToLower(s)
230+
231+
// Replace spaces with hyphens
232+
s = strings.ReplaceAll(s, " ", "-")
233+
234+
// Replace multiple hyphens with single hyphen
235+
re := regexp.MustCompile(`-+`)
236+
s = re.ReplaceAllString(s, "-")
237+
238+
// Remove any characters that are not alphanumeric or hyphens
239+
re = regexp.MustCompile(`[^a-z0-9-]`)
240+
s = re.ReplaceAllString(s, "")
241+
242+
// Trim leading and trailing hyphens
243+
s = strings.Trim(s, "-")
244+
245+
return s
246+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
sidebar_position: 101
3+
---
4+
5+
import Tabs from '@theme/Tabs';
6+
import TabItem from '@theme/TabItem';
7+
8+
# S3 Buckets
9+
10+
This example demonstrates how to create and manage AWS S3 buckets using kro's ResourceGraphDefinition.
11+
12+
## Overview
13+
14+
The S3 bucket example shows how to define an S3 bucket resource with common configurations such as versioning, encryption, and access policies. This is useful for applications that need object storage with consistent configuration across environments.
15+
## Manifest files
16+
17+
18+
19+
<Tabs>
20+
<TabItem value="instance" label="instance.yaml">
21+
22+
```kro
23+
apiVersion: kro.run/v1alpha1
24+
kind: S3Bucket
25+
metadata:
26+
name: s3demo
27+
namespace: default
28+
spec:
29+
name: s3demo-11223344
30+
```
31+
32+
</TabItem>
33+
<TabItem value="rg" label="rg.yaml">
34+
35+
```kro
36+
apiVersion: kro.run/v1alpha1
37+
kind: ResourceGraphDefinition
38+
metadata:
39+
name: s3bucket.kro.run
40+
spec:
41+
schema:
42+
apiVersion: v1alpha1
43+
kind: S3Bucket
44+
spec:
45+
name: string
46+
access: string | default="write"
47+
status:
48+
s3ARN: ${s3bucket.status.ackResourceMetadata.arn}
49+
s3PolicyARN: ${s3PolicyWrite.status.ackResourceMetadata.arn}
50+
51+
resources:
52+
- id: s3bucket
53+
template:
54+
apiVersion: s3.services.k8s.aws/v1alpha1
55+
kind: Bucket
56+
metadata:
57+
name: ${schema.spec.name}
58+
spec:
59+
name: ${schema.spec.name}
60+
- id: s3PolicyWrite
61+
includeWhen:
62+
- ${schema.spec.access == "write"}
63+
template:
64+
apiVersion: iam.services.k8s.aws/v1alpha1
65+
kind: Policy
66+
metadata:
67+
name: ${schema.spec.name}-s3-write-policy
68+
spec:
69+
name: ${schema.spec.name}-s3-write-policy
70+
policyDocument: |
71+
{
72+
"Version": "2012-10-17",
73+
"Statement": [
74+
{
75+
"Effect": "Allow",
76+
"Action": [
77+
"s3:GetObject",
78+
"s3:PutObject",
79+
"s3:PutObjectAcl",
80+
"s3:DeleteObject"
81+
],
82+
"Resource": [
83+
"${s3bucket.status.ackResourceMetadata.arn}/*"
84+
]
85+
},
86+
{
87+
"Effect": "Allow",
88+
"Action": [
89+
"s3:ListBucket",
90+
"s3:GetBucketLocation"
91+
],
92+
"Resource": [
93+
"${s3bucket.status.ackResourceMetadata.arn}"
94+
]
95+
}
96+
]
97+
}
98+
```
99+
100+
</TabItem>
101+
</Tabs>

0 commit comments

Comments
 (0)