Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

// +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

// 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()
}

Use in Template Generation

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

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

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.