diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 1645dbb07dd..ac1d67a289f 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -130,6 +130,7 @@ - [Extending](./plugins/extending.md) - [CLI and Plugins](./plugins/extending/extending_cli_features_and_plugins.md) - [External Plugins](./plugins/extending/external-plugins.md) + - [Custom Markers](./plugins/extending/custom-markers.md) - [E2E Tests](./plugins/extending/testing-plugins.md) - [Plugins Versioning](./plugins/plugins-versioning.md) diff --git a/docs/book/src/plugins/extending/custom-markers.md b/docs/book/src/plugins/extending/custom-markers.md new file mode 100644 index 00000000000..7b42d8b6bc3 --- /dev/null +++ b/docs/book/src/plugins/extending/custom-markers.md @@ -0,0 +1,221 @@ +# Creating Custom Markers + +## Overview + +When using Kubebuilder as a library, you may need to scaffold files with extensions that aren't natively supported by Kubebuilder's marker system. This guide shows you how to create custom marker support for any file extension. + +## When to Use Custom Markers + +Custom markers are useful when: + +- You're building an external plugin for languages not natively supported by Kubebuilder +- You want to scaffold files with custom extensions (`.rs`, `.java`, `.py`, `.tpl`, etc.) +- You need scaffolding markers in non-Go files for your own use cases +- Your file extensions aren't (and shouldn't be) part of the core `commentsByExt` map + +## Understanding Markers + +Markers are special comments used by Kubebuilder for scaffolding purposes. They indicate where code can be inserted or modified. The core Kubebuilder marker system only supports `.go`, `.yaml`, and `.yml` files by default. + +Example of a marker in a Go file: +```go +// +kubebuilder:scaffold:imports +``` + +## Implementation Example + +Here's how to implement custom markers for Rust files (`.rs`). This same pattern can be applied to any file extension. + +### Define Your Marker Type + +```go +// pkg/markers/rust.go +package markers + +import ( + "fmt" + "path/filepath" + "strings" +) + +const RustPluginPrefix = "+rust:scaffold:" + +type RustMarker struct { + prefix string + comment string + value string +} + +func NewRustMarker(path string, value string) (RustMarker, error) { + ext := filepath.Ext(path) + if ext != ".rs" { + return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext) + } + + return RustMarker{ + prefix: formatPrefix(RustPluginPrefix), + comment: "//", + value: value, + }, nil +} + +func (m RustMarker) String() string { + return m.comment + " " + m.prefix + m.value +} + +func formatPrefix(prefix string) string { + trimmed := strings.TrimSpace(prefix) + var builder strings.Builder + if !strings.HasPrefix(trimmed, "+") { + builder.WriteString("+") + } + builder.WriteString(trimmed) + if !strings.HasSuffix(trimmed, ":") { + builder.WriteString(":") + } + return builder.String() +} +``` + +### Use in Template Generation + +```go +package templates + +import ( + "fmt" + "github.com/yourorg/yourplugin/pkg/markers" +) + +func GenerateRustFile(projectName string) (string, error) { + marker, err := markers.NewRustMarker("src/main.rs", "imports") + if err != nil { + return "", err + } + + content := fmt.Sprintf(`// Generated by Rust Plugin +%s + +use std::error::Error; + +fn main() -> Result<(), Box> { + println!("Hello from %s!"); + Ok(()) +} +`, marker.String(), projectName) + + return content, nil +} + +func GenerateCargoToml(projectName string) string { + return fmt.Sprintf(`[package] +name = "%s" +version = "0.1.0" +edition = "2021" + +[dependencies] +`, projectName) +} +``` + +### Integrate with External Plugin + +```go +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" + "github.com/yourorg/yourplugin/pkg/markers" +) + +func main() { + // External plugins communicate via JSON over STDIN/STDOUT + reader := bufio.NewReader(os.Stdin) + input, err := io.ReadAll(reader) + if err != nil { + returnError(fmt.Errorf("error reading STDIN: %w", err)) + return + } + + pluginRequest := &external.PluginRequest{} + err = json.Unmarshal(input, pluginRequest) + if err != nil { + returnError(fmt.Errorf("error unmarshaling request: %w", err)) + return + } + + var response external.PluginResponse + + switch pluginRequest.Command { + case "init": + response = handleInit(pluginRequest) + default: + response = external.PluginResponse{ + Command: pluginRequest.Command, + Error: true, + ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)}, + } + } + + output, _ := json.Marshal(response) + fmt.Printf("%s", output) +} + +func handleInit(req *external.PluginRequest) external.PluginResponse { + // Create Rust file with custom markers + marker, err := markers.NewRustMarker("src/main.rs", "imports") + if err != nil { + return external.PluginResponse{ + Command: "init", + Error: true, + ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)}, + } + } + + fileContent := fmt.Sprintf(`// Generated by Rust Plugin +%s + +use std::error::Error; + +fn main() -> Result<(), Box> { + println!("Hello from Rust!"); + Ok(()) +} +`, marker.String()) + + // External plugins use "universe" to represent file changes + universe := make(map[string]string) + universe["src/main.rs"] = fileContent + + return external.PluginResponse{ + Command: "init", + Universe: universe, + } +} + +func returnError(err error) { + response := external.PluginResponse{ + Error: true, + ErrorMsgs: []string{err.Error()}, + } + output, _ := json.Marshal(response) + fmt.Printf("%s", output) +} +``` + +## Adapting for Other Languages + +To support other file extensions, modify the marker implementation by changing: + +- The comment syntax (e.g., `//` for Java, `#` for Python, `{{/* ... */}}` for templates) +- The file extension check (e.g., `.java`, `.py`, `.tpl`) +- The marker prefix (e.g., `+java:scaffold:`, `+python:scaffold:`) + +For more information on creating external plugins, see [External Plugins](external-plugins.md). \ No newline at end of file