Skip to content

Commit ddb8cdb

Browse files
committed
📖 Add docs: custom markers for unsupported file extensions
Fixes #4829
1 parent 9eafd53 commit ddb8cdb

File tree

2 files changed

+350
-0
lines changed

2 files changed

+350
-0
lines changed

docs/book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
- [Extending](./plugins/extending.md)
131131
- [CLI and Plugins](./plugins/extending/extending_cli_features_and_plugins.md)
132132
- [External Plugins](./plugins/extending/external-plugins.md)
133+
- [Custom Markers](./plugins/extending/custom-markers.md)
133134
- [E2E Tests](./plugins/extending/testing-plugins.md)
134135
- [Plugins Versioning](./plugins/plugins-versioning.md)
135136

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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

Comments
 (0)