Skip to content
Draft
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
7 changes: 7 additions & 0 deletions examples/aws/s3bucket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# S3 Buckets

This example demonstrates how to create and manage AWS S3 buckets using kro's ResourceGraphDefinition.

## Overview

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.
246 changes: 246 additions & 0 deletions scripts/generate-examples-doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//go:build ignore
// +build ignore

package main

import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"

"gopkg.in/yaml.v3"
)

const (
configFile = "website/examples.config.yaml"
outputBaseDir = "website/docs/examples"
)

// ExampleEntry represents a single example entry in the config
type ExampleEntry struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
SidebarPosition int `yaml:"sidebar_position"`
Files []FilePath `yaml:"files"`
}

// FilePath represents a path entry in the files list
type FilePath struct {
Path string `yaml:"path"`
}

// ExamplesConfig represents the entire configuration file
// The key is the category name (e.g., "kubernetes", "aws")
type ExamplesConfig map[string][]ExampleEntry

func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func run() error {
// Read the config file
config, err := readConfig(configFile)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}

// Process each category
for category, examples := range config {
fmt.Printf("Processing category: %s\n", category)

for idx, example := range examples {
sidebarPosition := example.SidebarPosition
if sidebarPosition == 0 {
sidebarPosition = idx + 1
}

for _, file := range example.Files {
// Resolve the path relative to the config file location
examplePath := filepath.Join(filepath.Dir(configFile), file.Path)

if err := processExample(category, example, examplePath, sidebarPosition); err != nil {
return fmt.Errorf("failed to process example %s: %w", example.Name, err)
}
}
}
}

fmt.Println("Documentation generation completed successfully!")
return nil
}

func readConfig(path string) (ExamplesConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
}

var config ExamplesConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}

return config, nil
}

func processExample(category string, example ExampleEntry, examplePath string, sidebarPosition int) error {
fmt.Printf(" Processing example: %s (path: %s)\n", example.Name, examplePath)

// Check if the example directory exists
info, err := os.Stat(examplePath)
if err != nil {
return fmt.Errorf("example path does not exist: %s: %w", examplePath, err)
}
if !info.IsDir() {
return fmt.Errorf("example path is not a directory: %s", examplePath)
}

// Read README.md
readmePath := filepath.Join(examplePath, "README.md")
readmeContent, err := os.ReadFile(readmePath)
if err != nil {
return fmt.Errorf("failed to read README.md at %s: %w", readmePath, err)
}

// Collect manifest files
manifestFiles, err := collectManifestFiles(examplePath)
if err != nil {
return fmt.Errorf("failed to collect manifest files: %w", err)
}

// Generate markdown content
markdownContent := generateMarkdown(string(readmeContent), manifestFiles, sidebarPosition)

// Determine output path
outputFileName := toKebabCase(example.Name) + ".md"
outputDir := filepath.Join(outputBaseDir, category)
outputPath := filepath.Join(outputDir, outputFileName)

// Ensure output directory exists
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
}

// Write the markdown file
if err := os.WriteFile(outputPath, []byte(markdownContent), 0644); err != nil {
return fmt.Errorf("failed to write output file %s: %w", outputPath, err)
}

fmt.Printf(" Generated: %s\n", outputPath)
return nil
}

// ManifestFile represents a manifest file with its name and content
type ManifestFile struct {
Name string
Content string
}

func collectManifestFiles(dirPath string) ([]ManifestFile, error) {
var manifestFiles []ManifestFile

entries, err := os.ReadDir(dirPath)
if err != nil {
return nil, fmt.Errorf("failed to read directory %s: %w", dirPath, err)
}

for _, entry := range entries {
if entry.IsDir() {
continue
}

name := entry.Name()
if strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") {
filePath := filepath.Join(dirPath, name)
content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
}

manifestFiles = append(manifestFiles, ManifestFile{
Name: name,
Content: string(content),
})
}
}

// Sort by filename for consistent output
sort.Slice(manifestFiles, func(i, j int) bool {
return manifestFiles[i].Name < manifestFiles[j].Name
})

return manifestFiles, nil
}

func generateMarkdown(readmeContent string, manifestFiles []ManifestFile, sidebarPosition int) string {
var sb strings.Builder

// Add frontmatter
sb.WriteString("---\n")
sb.WriteString(fmt.Sprintf("sidebar_position: %d\n", sidebarPosition))
sb.WriteString("---\n\n")

// Add Tabs import for Docusaurus
if len(manifestFiles) > 0 {
sb.WriteString("import Tabs from '@theme/Tabs';\n")
sb.WriteString("import TabItem from '@theme/TabItem';\n\n")
}

// Add README content
sb.WriteString(readmeContent)

// Add Manifest files in Tabs component
if len(manifestFiles) > 0 {
// Add Tab
sb.WriteString("## Manifest files\n\n")
sb.WriteString("\n\n<Tabs>\n")

for _, manifestFile := range manifestFiles {
// Generate a value identifier from filename (remove extension and special chars)
valueID := strings.TrimSuffix(manifestFile.Name, filepath.Ext(manifestFile.Name))
valueID = strings.ReplaceAll(valueID, ".", "-")

sb.WriteString(fmt.Sprintf(" <TabItem value=\"%s\" label=\"%s\">\n\n", valueID, manifestFile.Name))
sb.WriteString("```kro\n")
sb.WriteString(manifestFile.Content)
if !strings.HasSuffix(manifestFile.Content, "\n") {
sb.WriteString("\n")
}
sb.WriteString("```\n\n")
sb.WriteString(" </TabItem>\n")
}

sb.WriteString("</Tabs>\n")
}

return sb.String()
}

// toKebabCase converts a string to kebab-case
// e.g., "SaaS Multi Tenant" -> "saas-multi-tenant"
func toKebabCase(s string) string {
// Convert to lowercase
s = strings.ToLower(s)

// Replace spaces with hyphens
s = strings.ReplaceAll(s, " ", "-")

// Replace multiple hyphens with single hyphen
re := regexp.MustCompile(`-+`)
s = re.ReplaceAllString(s, "-")

// Remove any characters that are not alphanumeric or hyphens
re = regexp.MustCompile(`[^a-z0-9-]`)
s = re.ReplaceAllString(s, "")

// Trim leading and trailing hyphens
s = strings.Trim(s, "-")

return s
}
101 changes: 101 additions & 0 deletions website/docs/examples/aws/s3-bucket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
sidebar_position: 101
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# S3 Buckets

This example demonstrates how to create and manage AWS S3 buckets using kro's ResourceGraphDefinition.

## Overview

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.
## Manifest files



<Tabs>
<TabItem value="instance" label="instance.yaml">

```kro
apiVersion: kro.run/v1alpha1
kind: S3Bucket
metadata:
name: s3demo
namespace: default
spec:
name: s3demo-11223344
```

</TabItem>
<TabItem value="rg" label="rg.yaml">

```kro
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: s3bucket.kro.run
spec:
schema:
apiVersion: v1alpha1
kind: S3Bucket
spec:
name: string
access: string | default="write"
status:
s3ARN: ${s3bucket.status.ackResourceMetadata.arn}
s3PolicyARN: ${s3PolicyWrite.status.ackResourceMetadata.arn}

resources:
- id: s3bucket
template:
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: ${schema.spec.name}
spec:
name: ${schema.spec.name}
- id: s3PolicyWrite
includeWhen:
- ${schema.spec.access == "write"}
template:
apiVersion: iam.services.k8s.aws/v1alpha1
kind: Policy
metadata:
name: ${schema.spec.name}-s3-write-policy
spec:
name: ${schema.spec.name}-s3-write-policy
policyDocument: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:DeleteObject"
],
"Resource": [
"${s3bucket.status.ackResourceMetadata.arn}/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"${s3bucket.status.ackResourceMetadata.arn}"
]
}
]
}
```

</TabItem>
</Tabs>
Loading