docs/book/src/plugins/extending/custom-markers.md
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.
Custom markers are useful when:
.rs, .java, .py, .tpl, etc.)commentsByExt mapMarkers 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.
Example of a marker in a Go file:
// +kubebuilder:scaffold:imports
Here's how to implement custom markers for Rust files (.rs). This same pattern can be applied to any file extension.
// pkg/markers/rust.go
package markers
import (
"fmt"
"path/filepath"
"strings"
)
const RustPluginPrefix = "+rust:scaffold:"
type RustMarker struct {
prefix string
comment string
value string
}
func NewRustMarker(path string, value string) (RustMarker, error) {
ext := filepath.Ext(path)
if ext != ".rs" {
return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext)
}
return RustMarker{
prefix: formatPrefix(RustPluginPrefix),
comment: "//",
value: value,
}, nil
}
func (m RustMarker) String() string {
return m.comment + " " + m.prefix + m.value
}
func formatPrefix(prefix string) string {
trimmed := strings.TrimSpace(prefix)
var builder strings.Builder
if !strings.HasPrefix(trimmed, "+") {
builder.WriteString("+")
}
builder.WriteString(trimmed)
if !strings.HasSuffix(trimmed, ":") {
builder.WriteString(":")
}
return builder.String()
}
The formatPrefix implementation shown above is adapted from Kubebuilder's internal
markerPrefix function.
package templates
import (
"fmt"
"github.com/yourorg/yourplugin/pkg/markers"
)
func GenerateRustFile(projectName string) (string, error) {
marker, err := markers.NewRustMarker("src/main.rs", "imports")
if err != nil {
return "", err
}
content := fmt.Sprintf(`// Generated by Rust Plugin
%s
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
println!("Hello from %s!");
Ok(())
}
`, marker.String(), projectName)
return content, nil
}
func GenerateCargoToml(projectName string) string {
return fmt.Sprintf(`[package]
name = "%s"
version = "0.1.0"
edition = "2021"
[dependencies]
`, projectName)
}
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
"github.com/yourorg/yourplugin/pkg/markers"
)
func main() {
// External plugins communicate via JSON over STDIN/STDOUT
reader := bufio.NewReader(os.Stdin)
input, err := io.ReadAll(reader)
if err != nil {
returnError(fmt.Errorf("error reading STDIN: %w", err))
return
}
pluginRequest := &external.PluginRequest{}
err = json.Unmarshal(input, pluginRequest)
if err != nil {
returnError(fmt.Errorf("error unmarshaling request: %w", err))
return
}
var response external.PluginResponse
switch pluginRequest.Command {
case "init":
response = handleInit(pluginRequest)
default:
response = external.PluginResponse{
Command: pluginRequest.Command,
Error: true,
ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)},
}
}
output, err := json.Marshal(response)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to marshal response: %v\n", err)
os.Exit(1)
}
fmt.Printf("%s", output)
}
func handleInit(req *external.PluginRequest) external.PluginResponse {
// Create Rust file with custom markers
marker, err := markers.NewRustMarker("src/main.rs", "imports")
if err != nil {
return external.PluginResponse{
Command: "init",
Error: true,
ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)},
}
}
fileContent := fmt.Sprintf(`// Generated by Rust Plugin
%s
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
println!("Hello from Rust!");
Ok(())
}
`, marker.String())
// External plugins use "universe" to represent file changes.
// "universe" is a map from file paths to their file contents,
// passed through the plugin chain to coordinate file generation.
universe := make(map[string]string)
universe["src/main.rs"] = fileContent
return external.PluginResponse{
Command: "init",
Universe: universe,
}
}
func returnError(err error) {
response := external.PluginResponse{
Error: true,
ErrorMsgs: []string{err.Error()},
}
output, marshalErr := json.Marshal(response)
if marshalErr != nil {
fmt.Fprintf(os.Stderr, "failed to marshal error response: %v\n", marshalErr)
os.Exit(1)
}
fmt.Printf("%s", output)
}
To support other file extensions, modify the marker implementation by changing:
// for Java, # for Python, {} for templates).java, .py, .tpl)+java:scaffold:, +python:scaffold:)For more information on creating external plugins, see External Plugins.