Skip to content

Commit 44a900a

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
1 parent e2bc0ac commit 44a900a

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

docs/book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
- [Extending](./plugins/extending.md)
129129
- [CLI and Plugins](./plugins/extending/extending_cli_features_and_plugins.md)
130130
- [External Plugins](./plugins/extending/external-plugins.md)
131+
- [Custom Markers](./plugins/extending/custom-markers.md)
131132
- [E2E Tests](./plugins/extending/testing-plugins.md)
132133
- [Plugins Versioning](./plugins/plugins-versioning.md)
133134

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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+
80+
<aside class="note">
81+
<h1>Implementation Reference</h1>
82+
83+
The `formatPrefix` implementation shown above is adapted from Kubebuilder's internal
84+
[markerPrefix function](https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/machinery/marker.go).
85+
86+
</aside>
87+
88+
### Use in Template Generation
89+
90+
```go
91+
package templates
92+
93+
import (
94+
"fmt"
95+
"github.com/yourorg/yourplugin/pkg/markers"
96+
)
97+
98+
func GenerateRustFile(projectName string) (string, error) {
99+
marker, err := markers.NewRustMarker("src/main.rs", "imports")
100+
if err != nil {
101+
return "", err
102+
}
103+
104+
content := fmt.Sprintf(`// Generated by Rust Plugin
105+
%s
106+
107+
use std::error::Error;
108+
109+
fn main() -> Result<(), Box<dyn Error>> {
110+
println!("Hello from %s!");
111+
Ok(())
112+
}
113+
`, marker.String(), projectName)
114+
115+
return content, nil
116+
}
117+
118+
func GenerateCargoToml(projectName string) string {
119+
return fmt.Sprintf(`[package]
120+
name = "%s"
121+
version = "0.1.0"
122+
edition = "2021"
123+
124+
[dependencies]
125+
`, projectName)
126+
}
127+
```
128+
129+
### Integrate with External Plugin
130+
131+
```go
132+
package main
133+
134+
import (
135+
"bufio"
136+
"encoding/json"
137+
"fmt"
138+
"io"
139+
"os"
140+
141+
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
142+
"github.com/yourorg/yourplugin/pkg/markers"
143+
)
144+
145+
func main() {
146+
// External plugins communicate via JSON over STDIN/STDOUT
147+
reader := bufio.NewReader(os.Stdin)
148+
input, err := io.ReadAll(reader)
149+
if err != nil {
150+
returnError(fmt.Errorf("error reading STDIN: %w", err))
151+
return
152+
}
153+
154+
pluginRequest := &external.PluginRequest{}
155+
err = json.Unmarshal(input, pluginRequest)
156+
if err != nil {
157+
returnError(fmt.Errorf("error unmarshaling request: %w", err))
158+
return
159+
}
160+
161+
var response external.PluginResponse
162+
163+
switch pluginRequest.Command {
164+
case "init":
165+
response = handleInit(pluginRequest)
166+
default:
167+
response = external.PluginResponse{
168+
Command: pluginRequest.Command,
169+
Error: true,
170+
ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)},
171+
}
172+
}
173+
174+
output, err := json.Marshal(response)
175+
if err != nil {
176+
fmt.Fprintf(os.Stderr, "failed to marshal response: %v\n", err)
177+
os.Exit(1)
178+
}
179+
fmt.Printf("%s", output)
180+
}
181+
182+
func handleInit(req *external.PluginRequest) external.PluginResponse {
183+
// Create Rust file with custom markers
184+
marker, err := markers.NewRustMarker("src/main.rs", "imports")
185+
if err != nil {
186+
return external.PluginResponse{
187+
Command: "init",
188+
Error: true,
189+
ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)},
190+
}
191+
}
192+
193+
fileContent := fmt.Sprintf(`// Generated by Rust Plugin
194+
%s
195+
196+
use std::error::Error;
197+
198+
fn main() -> Result<(), Box<dyn Error>> {
199+
println!("Hello from Rust!");
200+
Ok(())
201+
}
202+
`, marker.String())
203+
204+
// External plugins use "universe" to represent file changes.
205+
// "universe" is a map from file paths to their file contents,
206+
// passed through the plugin chain to coordinate file generation.
207+
universe := make(map[string]string)
208+
universe["src/main.rs"] = fileContent
209+
210+
return external.PluginResponse{
211+
Command: "init",
212+
Universe: universe,
213+
}
214+
}
215+
216+
func returnError(err error) {
217+
response := external.PluginResponse{
218+
Error: true,
219+
ErrorMsgs: []string{err.Error()},
220+
}
221+
output, marshalErr := json.Marshal(response)
222+
if marshalErr != nil {
223+
fmt.Fprintf(os.Stderr, "failed to marshal error response: %v\n", marshalErr)
224+
os.Exit(1)
225+
}
226+
fmt.Printf("%s", output)
227+
}
228+
```
229+
230+
## Adapting for Other Languages
231+
232+
To support other file extensions, modify the marker implementation by changing:
233+
234+
- The comment syntax (e.g., `//` for Java, `#` for Python, `{{/* ... */}}` for templates)
235+
- The file extension check (e.g., `.java`, `.py`, `.tpl`)
236+
- The marker prefix (e.g., `+java:scaffold:`, `+python:scaffold:`)
237+
238+
For more information on creating external plugins, see [External Plugins](external-plugins.md).

0 commit comments

Comments
 (0)