Skip to content

Commit c51a0a6

Browse files
committed
📖 Add docs: Creating custom markers for unsupported file extensions
Shows how to use Kubebuilder as a library to create custom marker support for external plugins with non-Go file extensions like .rs, .java, .py - Adds comprehensive guide for implementing custom markers - Provides working code examples for Rust-based external plugins - Includes integration patterns for external plugin communication - Addresses all PR review feedback and Copilot suggestions - Fixes trailing whitespace and code quality issues Fixes #4829
1 parent eb935e8 commit c51a0a6

File tree

2 files changed

+235
-0
lines changed

2 files changed

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

Comments
 (0)