|
| 1 | +# Creating Custom Markers |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## When to Use Custom Markers |
| 8 | + |
| 9 | +Custom markers are useful when: |
| 10 | + |
| 11 | +- You're building an external plugin for languages not natively supported by Kubebuilder |
| 12 | +- You want to scaffold files with custom extensions (`.rs`, `.java`, `.py`, `.tpl`, etc.) |
| 13 | +- You need scaffolding markers in non-Go files for your own use cases |
| 14 | +- Your file extensions aren't (and shouldn't be) part of the core `commentsByExt` map |
| 15 | + |
| 16 | +## Understanding Markers |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +Example of a marker in a Go file: |
| 21 | +```go |
| 22 | +// +kubebuilder:scaffold:imports |
| 23 | +``` |
| 24 | + |
| 25 | +## Implementation Example |
| 26 | + |
| 27 | +Here's how to implement custom markers for Rust files (`.rs`). This same pattern can be applied to any file extension. |
| 28 | + |
| 29 | +### Define Your Marker Type |
| 30 | + |
| 31 | +```go |
| 32 | +// pkg/markers/rust.go |
| 33 | +package markers |
| 34 | + |
| 35 | +import ( |
| 36 | + "fmt" |
| 37 | + "path/filepath" |
| 38 | + "strings" |
| 39 | +) |
| 40 | + |
| 41 | +const RustPluginPrefix = "+rust:scaffold:" |
| 42 | + |
| 43 | +type RustMarker struct { |
| 44 | + prefix string |
| 45 | + comment string |
| 46 | + value string |
| 47 | +} |
| 48 | + |
| 49 | +func NewRustMarker(path string, value string) (RustMarker, error) { |
| 50 | + ext := filepath.Ext(path) |
| 51 | + if ext != ".rs" { |
| 52 | + return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext) |
| 53 | + } |
| 54 | + |
| 55 | + return RustMarker{ |
| 56 | + prefix: formatPrefix(RustPluginPrefix), |
| 57 | + comment: "//", |
| 58 | + value: value, |
| 59 | + }, nil |
| 60 | +} |
| 61 | + |
| 62 | +func (m RustMarker) String() string { |
| 63 | + return m.comment + " " + m.prefix + m.value |
| 64 | +} |
| 65 | + |
| 66 | +func formatPrefix(prefix string) string { |
| 67 | + trimmed := strings.TrimSpace(prefix) |
| 68 | + var builder strings.Builder |
| 69 | + if !strings.HasPrefix(trimmed, "+") { |
| 70 | + builder.WriteString("+") |
| 71 | + } |
| 72 | + builder.WriteString(trimmed) |
| 73 | + if !strings.HasSuffix(trimmed, ":") { |
| 74 | + builder.WriteString(":") |
| 75 | + } |
| 76 | + return builder.String() |
| 77 | +} |
| 78 | + |
| 79 | +// Note: This formatPrefix implementation is adapted from Kubebuilder's internal |
| 80 | +// markerPrefix function in pkg/machinery/marker.go |
| 81 | +``` |
| 82 | + |
| 83 | +### Use in Template Generation |
| 84 | + |
| 85 | +```go |
| 86 | +package templates |
| 87 | + |
| 88 | +import ( |
| 89 | + "fmt" |
| 90 | + "github.com/yourorg/yourplugin/pkg/markers" |
| 91 | +) |
| 92 | + |
| 93 | +func GenerateRustFile(projectName string) (string, error) { |
| 94 | + marker, err := markers.NewRustMarker("src/main.rs", "imports") |
| 95 | + if err != nil { |
| 96 | + return "", err |
| 97 | + } |
| 98 | + |
| 99 | + content := fmt.Sprintf(`// Generated by Rust Plugin |
| 100 | +%s |
| 101 | +
|
| 102 | +use std::error::Error; |
| 103 | +
|
| 104 | +fn main() -> Result<(), Box<dyn Error>> { |
| 105 | + println!("Hello from %s!"); |
| 106 | + Ok(()) |
| 107 | +} |
| 108 | +`, marker.String(), projectName) |
| 109 | + |
| 110 | + return content, nil |
| 111 | +} |
| 112 | + |
| 113 | +func GenerateCargoToml(projectName string) string { |
| 114 | + return fmt.Sprintf(`[package] |
| 115 | +name = "%s" |
| 116 | +version = "0.1.0" |
| 117 | +edition = "2021" |
| 118 | +
|
| 119 | +[dependencies] |
| 120 | +`, projectName) |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### Integrate with External Plugin |
| 125 | + |
| 126 | +```go |
| 127 | +package main |
| 128 | + |
| 129 | +import ( |
| 130 | + "bufio" |
| 131 | + "encoding/json" |
| 132 | + "fmt" |
| 133 | + "io" |
| 134 | + "os" |
| 135 | + "path/filepath" |
| 136 | + |
| 137 | + "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" |
| 138 | + "github.com/yourorg/yourplugin/pkg/markers" |
| 139 | +) |
| 140 | + |
| 141 | +func main() { |
| 142 | + // External plugins communicate via JSON over STDIN/STDOUT |
| 143 | + reader := bufio.NewReader(os.Stdin) |
| 144 | + input, err := io.ReadAll(reader) |
| 145 | + if err != nil { |
| 146 | + returnError(fmt.Errorf("error reading STDIN: %w", err)) |
| 147 | + return |
| 148 | + } |
| 149 | + |
| 150 | + pluginRequest := &external.PluginRequest{} |
| 151 | + err = json.Unmarshal(input, pluginRequest) |
| 152 | + if err != nil { |
| 153 | + returnError(fmt.Errorf("error unmarshaling request: %w", err)) |
| 154 | + return |
| 155 | + } |
| 156 | + |
| 157 | + var response external.PluginResponse |
| 158 | + |
| 159 | + switch pluginRequest.Command { |
| 160 | + case "init": |
| 161 | + response = handleInit(pluginRequest) |
| 162 | + default: |
| 163 | + response = external.PluginResponse{ |
| 164 | + Command: pluginRequest.Command, |
| 165 | + Error: true, |
| 166 | + ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)}, |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + output, err := json.Marshal(response) |
| 171 | + if err != nil { |
| 172 | + fmt.Fprintf(os.Stderr, "failed to marshal response: %v\n", err) |
| 173 | + os.Exit(1) |
| 174 | + } |
| 175 | + fmt.Printf("%s", output) |
| 176 | +} |
| 177 | + |
| 178 | +func handleInit(req *external.PluginRequest) external.PluginResponse { |
| 179 | + // Create Rust file with custom markers |
| 180 | + marker, err := markers.NewRustMarker("src/main.rs", "imports") |
| 181 | + if err != nil { |
| 182 | + return external.PluginResponse{ |
| 183 | + Command: "init", |
| 184 | + Error: true, |
| 185 | + ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)}, |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + fileContent := fmt.Sprintf(`// Generated by Rust Plugin |
| 190 | +%s |
| 191 | +
|
| 192 | +use std::error::Error; |
| 193 | +
|
| 194 | +fn main() -> Result<(), Box<dyn Error>> { |
| 195 | + println!("Hello from Rust!"); |
| 196 | + Ok(()) |
| 197 | +} |
| 198 | +`, marker.String()) |
| 199 | + |
| 200 | + // External plugins use "universe" to represent file changes. |
| 201 | + // "universe" is a map from file paths to their file contents, |
| 202 | + // passed through the plugin chain to coordinate file generation. |
| 203 | + universe := make(map[string]string) |
| 204 | + universe["src/main.rs"] = fileContent |
| 205 | + |
| 206 | + return external.PluginResponse{ |
| 207 | + Command: "init", |
| 208 | + Universe: universe, |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +func returnError(err error) { |
| 213 | + response := external.PluginResponse{ |
| 214 | + Error: true, |
| 215 | + ErrorMsgs: []string{err.Error()}, |
| 216 | + } |
| 217 | + output, marshalErr := json.Marshal(response) |
| 218 | + if marshalErr != nil { |
| 219 | + fmt.Fprintf(os.Stderr, "failed to marshal error response: %v\n", marshalErr) |
| 220 | + os.Exit(1) |
| 221 | + } |
| 222 | + fmt.Printf("%s", output) |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +## Adapting for Other Languages |
| 227 | + |
| 228 | +To support other file extensions, modify the marker implementation by changing: |
| 229 | + |
| 230 | +- The comment syntax (e.g., `//` for Java, `#` for Python, `{{/* ... */}}` for templates) |
| 231 | +- The file extension check (e.g., `.java`, `.py`, `.tpl`) |
| 232 | +- The marker prefix (e.g., `+java:scaffold:`, `+python:scaffold:`) |
| 233 | + |
| 234 | +For more information on creating external plugins, see [External Plugins](external-plugins.md). |
0 commit comments