|
| 1 | +# Creating Custom Markers for Unsupported File Extensions |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +When building external plugins for Kubebuilder, you may need to scaffold files with extensions that aren't natively supported by Kubebuilder's marker system (such as `.rs` for Rust, `.java` for Java, `.tpl` for templates, etc.). This guide shows you how to use Kubebuilder as a library to create your own 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 a language not natively supported by Kubebuilder |
| 12 | +- You want to scaffold files with custom extensions (`.rs`, `.java`, `.tpl`, `.py`, etc.) |
| 13 | +- Your file extensions aren't (and shouldn't be) part of the core `commentsByExt` map |
| 14 | +- You need to maintain scaffolding markers in non-Go files |
| 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 | +## Creating Custom Markers |
| 26 | + |
| 27 | +Instead of modifying Kubebuilder's core code, you can create your own marker implementation using Kubebuilder as a library. Here's how to implement custom markers for your specific file extensions. |
| 28 | + |
| 29 | +### Basic Custom Marker Implementation |
| 30 | + |
| 31 | +```go |
| 32 | +package plugin |
| 33 | + |
| 34 | +import ( |
| 35 | + "fmt" |
| 36 | + "path/filepath" |
| 37 | + "strings" |
| 38 | + |
| 39 | + "sigs.k8s.io/kubebuilder/v4/pkg/machinery" |
| 40 | +) |
| 41 | + |
| 42 | +// Define your custom prefix for markers |
| 43 | +const myPluginPrefix = "+myplugin:scaffold:" |
| 44 | + |
| 45 | +// Map file extensions to their comment syntax |
| 46 | +var customCommentsByExt = map[string]string{ |
| 47 | + ".rs": "//", // Rust |
| 48 | + ".java": "//", // Java |
| 49 | + ".py": "#", // Python |
| 50 | + ".rb": "#", // Ruby |
| 51 | + ".tpl": "{{/* ", // Template files (with closing */}}) |
| 52 | +} |
| 53 | + |
| 54 | +// CustomMarker extends the base Marker functionality |
| 55 | +type CustomMarker struct { |
| 56 | + prefix string |
| 57 | + comment string |
| 58 | + value string |
| 59 | +} |
| 60 | + |
| 61 | +// NewCustomMarker creates a marker for your custom file extension |
| 62 | +func NewCustomMarker(path string, value string) (CustomMarker, error) { |
| 63 | + ext := filepath.Ext(path) |
| 64 | + comment, ok := customCommentsByExt[ext] |
| 65 | + if !ok { |
| 66 | + return CustomMarker{}, fmt.Errorf("unsupported file extension: %s", ext) |
| 67 | + } |
| 68 | + |
| 69 | + return CustomMarker{ |
| 70 | + prefix: markerPrefix(myPluginPrefix), |
| 71 | + comment: comment, |
| 72 | + value: value, |
| 73 | + }, nil |
| 74 | +} |
| 75 | + |
| 76 | +// String implements the Stringer interface |
| 77 | +func (m CustomMarker) String() string { |
| 78 | + // Special handling for template files |
| 79 | + if m.comment == "{{/* " { |
| 80 | + return m.comment + m.prefix + m.value + " */}}" |
| 81 | + } |
| 82 | + return m.comment + " " + m.prefix + m.value |
| 83 | +} |
| 84 | + |
| 85 | +// EqualsLine checks if a line contains this marker |
| 86 | +func (m CustomMarker) EqualsLine(line string) bool { |
| 87 | + line = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), m.comment)) |
| 88 | + return line == m.prefix+m.value |
| 89 | +} |
| 90 | + |
| 91 | +// Helper function to format the marker prefix |
| 92 | +func markerPrefix(prefix string) string { |
| 93 | + trimmed := strings.TrimSpace(prefix) |
| 94 | + var builder strings.Builder |
| 95 | + if !strings.HasPrefix(trimmed, "+") { |
| 96 | + builder.WriteString("+") |
| 97 | + } |
| 98 | + builder.WriteString(trimmed) |
| 99 | + if !strings.HasSuffix(trimmed, ":") { |
| 100 | + builder.WriteString(":") |
| 101 | + } |
| 102 | + return builder.String() |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +## Complete Example: Rust Plugin with Custom Markers |
| 107 | + |
| 108 | +Here's a complete example showing how to create an external plugin for Rust with custom marker support: |
| 109 | + |
| 110 | +### 1. Define Your Marker System |
| 111 | + |
| 112 | +```go |
| 113 | +// pkg/markers/rust.go |
| 114 | +package markers |
| 115 | + |
| 116 | +import ( |
| 117 | + "fmt" |
| 118 | + "path/filepath" |
| 119 | + "strings" |
| 120 | +) |
| 121 | + |
| 122 | +const RustPluginPrefix = "+rust:scaffold:" |
| 123 | + |
| 124 | +type RustMarker struct { |
| 125 | + prefix string |
| 126 | + comment string |
| 127 | + value string |
| 128 | +} |
| 129 | + |
| 130 | +func NewRustMarker(path string, value string) (RustMarker, error) { |
| 131 | + ext := filepath.Ext(path) |
| 132 | + if ext != ".rs" { |
| 133 | + return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext) |
| 134 | + } |
| 135 | + |
| 136 | + return RustMarker{ |
| 137 | + prefix: formatPrefix(RustPluginPrefix), |
| 138 | + comment: "//", |
| 139 | + value: value, |
| 140 | + }, nil |
| 141 | +} |
| 142 | + |
| 143 | +func (m RustMarker) String() string { |
| 144 | + return m.comment + " " + m.prefix + m.value |
| 145 | +} |
| 146 | + |
| 147 | +func formatPrefix(prefix string) string { |
| 148 | + trimmed := strings.TrimSpace(prefix) |
| 149 | + var builder strings.Builder |
| 150 | + if !strings.HasPrefix(trimmed, "+") { |
| 151 | + builder.WriteString("+") |
| 152 | + } |
| 153 | + builder.WriteString(trimmed) |
| 154 | + if !strings.HasSuffix(trimmed, ":") { |
| 155 | + builder.WriteString(":") |
| 156 | + } |
| 157 | + return builder.String() |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +### 2. Create Scaffolding Templates |
| 162 | + |
| 163 | +```go |
| 164 | +// pkg/templates/main.go |
| 165 | +package templates |
| 166 | + |
| 167 | +import ( |
| 168 | + "path/filepath" |
| 169 | + "sigs.k8s.io/kubebuilder/v4/pkg/machinery" |
| 170 | + "github.com/yourorg/yourplugin/pkg/markers" |
| 171 | +) |
| 172 | + |
| 173 | +type RustMainFile struct { |
| 174 | + machinery.TemplateMixin |
| 175 | + machinery.ProjectNameMixin |
| 176 | + |
| 177 | + // Add any additional fields your template needs |
| 178 | + ModuleName string |
| 179 | +} |
| 180 | + |
| 181 | +// SetTemplateDefaults sets default values for the template |
| 182 | +func (f *RustMainFile) SetTemplateDefaults() error { |
| 183 | + if f.Path == "" { |
| 184 | + f.Path = filepath.Join("src", "main.rs") |
| 185 | + } |
| 186 | + |
| 187 | + // Create the marker for where additional code can be scaffolded |
| 188 | + marker, err := markers.NewRustMarker(f.Path, "imports") |
| 189 | + if err != nil { |
| 190 | + return err |
| 191 | + } |
| 192 | + |
| 193 | + f.TemplateBody = fmt.Sprintf(`// Generated by Kubebuilder Rust Plugin |
| 194 | +%s |
| 195 | +
|
| 196 | +use std::error::Error; |
| 197 | +
|
| 198 | +%s |
| 199 | +
|
| 200 | +fn main() -> Result<(), Box<dyn Error>> { |
| 201 | + println!("Hello from %s!"); |
| 202 | + Ok(()) |
| 203 | +} |
| 204 | +`, marker.String(), marker.String(), f.ProjectName) |
| 205 | + |
| 206 | + return nil |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +### 3. Integrate with External Plugin |
| 211 | + |
| 212 | +```go |
| 213 | +// cmd/main.go |
| 214 | +package main |
| 215 | + |
| 216 | +import ( |
| 217 | + "sigs.k8s.io/kubebuilder/v4/pkg/plugin" |
| 218 | + "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" |
| 219 | + "github.com/yourorg/yourplugin/pkg/templates" |
| 220 | +) |
| 221 | + |
| 222 | +func main() { |
| 223 | + // Create and run your external plugin |
| 224 | + p := &external.Plugin{ |
| 225 | + Name: "rust.kubebuilder.io", |
| 226 | + Version: plugin.Version{Number: 1, Stage: plugin.Alpha}, |
| 227 | + |
| 228 | + Init: func(req external.PluginRequest) external.PluginResponse { |
| 229 | + // Create the main.rs file with markers |
| 230 | + mainFile := &templates.RustMainFile{ |
| 231 | + ModuleName: "my_operator", |
| 232 | + } |
| 233 | + |
| 234 | + return external.PluginResponse{ |
| 235 | + Universe: req.Universe, |
| 236 | + Files: []machinery.File{mainFile}, |
| 237 | + } |
| 238 | + }, |
| 239 | + } |
| 240 | + |
| 241 | + external.Run(p) |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +## Additional Examples |
| 246 | + |
| 247 | +### Java Markers |
| 248 | + |
| 249 | +```go |
| 250 | +type JavaMarker struct { |
| 251 | + prefix string |
| 252 | + comment string |
| 253 | + value string |
| 254 | +} |
| 255 | + |
| 256 | +func NewJavaMarker(path string, value string) (JavaMarker, error) { |
| 257 | + ext := filepath.Ext(path) |
| 258 | + if ext != ".java" { |
| 259 | + return JavaMarker{}, fmt.Errorf("expected .java file, got %s", ext) |
| 260 | + } |
| 261 | + |
| 262 | + return JavaMarker{ |
| 263 | + prefix: "+java:scaffold:", |
| 264 | + comment: "//", |
| 265 | + value: value, |
| 266 | + }, nil |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +### Template File Markers |
| 271 | + |
| 272 | +```go |
| 273 | +type TemplateMarker struct { |
| 274 | + prefix string |
| 275 | + value string |
| 276 | +} |
| 277 | + |
| 278 | +func NewTemplateMarker(path string, value string) (TemplateMarker, error) { |
| 279 | + ext := filepath.Ext(path) |
| 280 | + if ext != ".tpl" && ext != ".tmpl" { |
| 281 | + return TemplateMarker{}, fmt.Errorf("expected template file, got %s", ext) |
| 282 | + } |
| 283 | + |
| 284 | + return TemplateMarker{ |
| 285 | + prefix: "+template:scaffold:", |
| 286 | + value: value, |
| 287 | + }, nil |
| 288 | +} |
| 289 | + |
| 290 | +func (m TemplateMarker) String() string { |
| 291 | + return fmt.Sprintf("{{/* %s%s */}}", m.prefix, m.value) |
| 292 | +} |
| 293 | +``` |
| 294 | + |
| 295 | +## Best Practices |
| 296 | + |
| 297 | +1. **Use Clear Prefixes**: Choose a unique prefix for your plugin to avoid conflicts (e.g., `+rust:scaffold:`, `+java:scaffold:`) |
| 298 | + |
| 299 | +2. **Handle Comments Correctly**: Different languages have different comment syntax. Make sure to map the correct comment style for each file extension. |
| 300 | + |
| 301 | +3. **Error Handling**: Always validate file extensions and return clear error messages when unsupported files are encountered. |
| 302 | + |
| 303 | +4. **Maintain Compatibility**: When possible, follow the same patterns as Kubebuilder's core marker system to maintain consistency. |
| 304 | + |
| 305 | +5. **Document Your Markers**: Clearly document what markers your plugin supports and where they should be placed. |
| 306 | + |
| 307 | +6. **Testing**: Test your marker implementation with various file types and edge cases. |
| 308 | + |
| 309 | +## Integration with Scaffolding |
| 310 | + |
| 311 | +When using custom markers in your scaffolding logic: |
| 312 | + |
| 313 | +```go |
| 314 | +func (s *Scaffolder) Execute() error { |
| 315 | + // Read existing file |
| 316 | + content, err := afero.ReadFile(s.fs, "src/main.rs") |
| 317 | + if err != nil { |
| 318 | + return err |
| 319 | + } |
| 320 | + |
| 321 | + // Create marker |
| 322 | + marker, err := markers.NewRustMarker("src/main.rs", "imports") |
| 323 | + if err != nil { |
| 324 | + return err |
| 325 | + } |
| 326 | + |
| 327 | + // Find marker in content |
| 328 | + lines := strings.Split(string(content), "\n") |
| 329 | + for i, line := range lines { |
| 330 | + if strings.Contains(line, marker.String()) { |
| 331 | + // Insert new code after marker |
| 332 | + newCode := "use my_crate::MyStruct;" |
| 333 | + lines = append(lines[:i+1], |
| 334 | + append([]string{newCode}, lines[i+1:]...)...) |
| 335 | + break |
| 336 | + } |
| 337 | + } |
| 338 | + |
| 339 | + // Write back to file |
| 340 | + return afero.WriteFile(s.fs, "src/main.rs", |
| 341 | + []byte(strings.Join(lines, "\n")), 0644) |
| 342 | +} |
| 343 | +``` |
| 344 | + |
| 345 | +## Conclusion |
| 346 | + |
| 347 | +By creating custom markers, you can extend Kubebuilder's scaffolding capabilities to any language or file type without modifying Kubebuilder's core code. This approach maintains clean separation of concerns and allows external plugin developers to have full control over their scaffolding logic. |
| 348 | + |
| 349 | +For more information on creating external plugins, see [External Plugins](external-plugins.md). |
0 commit comments