Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
- [Extending](./plugins/extending.md)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the commit has been marked as invalid.
Could you please check if it contains an invalid character or something similar?

- [CLI and Plugins](./plugins/extending/extending_cli_features_and_plugins.md)
- [External Plugins](./plugins/extending/external-plugins.md)
- [Custom Markers](./plugins/extending/custom-markers.md)
- [E2E Tests](./plugins/extending/testing-plugins.md)
- [Plugins Versioning](./plugins/plugins-versioning.md)

Expand Down
234 changes: 234 additions & 0 deletions docs/book/src/plugins/extending/custom-markers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Creating Custom Markers

## Overview

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.

## When to Use Custom Markers

Custom markers are useful when:

- You're building an external plugin for languages not natively supported by Kubebuilder
- You want to scaffold files with custom extensions (`.rs`, `.java`, `.py`, `.tpl`, etc.)
- You need scaffolding markers in non-Go files for your own use cases
- Your file extensions aren't (and shouldn't be) part of the core `commentsByExt` map

## Understanding Markers

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.

Example of a marker in a Go file:
```go
// +kubebuilder:scaffold:imports
```

## Implementation Example

Here's how to implement custom markers for Rust files (`.rs`). This same pattern can be applied to any file extension.

### Define Your Marker Type

```go
// 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()
}
Comment on lines +66 to +77
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatPrefix function duplicates logic from Kubebuilder's internal markerPrefix function in pkg/machinery/marker.go. Consider mentioning in the documentation that this is adapted from Kubebuilder's internal implementation, or reference the actual implementation pattern to help maintainers understand the source of this logic.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nerdeveloper I think it is a nice review


// Note: This formatPrefix implementation is adapted from Kubebuilder's internal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a class for note.
Could you please check it out?
Example;

<aside class="note warning">
<h1>Automation process will involve deleting all files to regenerate</h1>
Deletes all files except `.git` and `PROJECT`.
</aside>

Also, remove from the block of code and link the Kubebuilder implementation?

// markerPrefix function in pkg/machinery/marker.go
```

### Use in Template Generation

```go
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)
}
```

### Integrate with External Plugin

```go
package main

import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path/filepath import on line 135 is unused in this example. It should be removed to avoid confusion, as filepath is only imported but never used in the main function or any of its helper functions.

Suggested change
"path/filepath"

Copilot uses AI. Check for mistakes.

"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)
}
```

## Adapting for Other Languages

To support other file extensions, modify the marker implementation by changing:

- The comment syntax (e.g., `//` for Java, `#` for Python, `{{/* ... */}}` for templates)
- The file extension check (e.g., `.java`, `.py`, `.tpl`)
- The marker prefix (e.g., `+java:scaffold:`, `+python:scaffold:`)

For more information on creating external plugins, see [External Plugins](external-plugins.md).