Creating Your First Extension
Learn how to build a custom r2r extension - a containerized command that extends the r2r CLI with your own functionality.
Prerequisites: Quick Start Guide, Docker installed, basic Go knowledge
What You'll Learn
By the end of this tutorial, you'll be able to:
- Understand what r2r extensions are and how they work
- Set up a proper Go project structure for extensions
- Implement the required extension metadata
- Build and test your extension locally
- Use your extension via the r2r CLI
What is an Extension?
An r2r extension is a Docker container that adds custom commands to the r2r CLI. Extensions run in isolation and receive arguments from the CLI.
Key characteristics:
- Containerized - Packaged as Docker images
- CLI-integrated - Invoked via
r2r <extension-name> <command> - Self-describing - Provide metadata about capabilities and requirements
- Portable - Run consistently across platforms
Example: The ext-env-check extension validates environment variables:
Complete Guide Available
This tutorial provides a quick introduction. For comprehensive details, see:
- Creating Extensions Guide - Complete reference with production patterns
- ext-env-check Repository - Working example to study
Step 1: Create Project Structure
Create a new directory for your extension:
Set up the recommended structure:
my-extension/
├── go/
│ └── my-extension/
│ ├── cmd/
│ │ └── my-extension/
│ │ └── main.go
│ ├── internal/
│ │ ├── metadata/
│ │ │ └── metadata.go
│ │ └── core/
│ │ └── logic.go
│ ├── go.mod
│ └── go.sum
└── containers/
└── my-extension/
└── Dockerfile
Why this structure?
cmd/- Entry point separate from logicinternal/- Encapsulated packagesinternal/metadata/- Extension metadata implementationinternal/core/- Business logiccontainers/- Docker configuration
Step 2: Implement Extension Metadata
Every extension must implement the extension-meta command.
Create go/my-extension/internal/metadata/metadata.go:
package metadata
import (
"encoding/json"
"os"
)
type Metadata struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
SchemaVersion string `json:"schema-version"`
Capabilities []string `json:"capabilities"`
Requirements Requirements `json:"requirements"`
}
type Requirements struct {
R2RVersion string `json:"r2r-version"`
ContainerRuntime string `json:"container-runtime"`
MinimumMemory string `json:"minimum-memory"`
MinimumCPU string `json:"minimum-cpu"`
}
func Print(version string) {
meta := Metadata{
Name: "my-extension",
Version: version,
Description: "My custom extension",
SchemaVersion: "1.0",
Capabilities: []string{"custom-task"},
Requirements: Requirements{
R2RVersion: ">=0.1.0",
ContainerRuntime: "docker",
MinimumMemory: "64MB",
MinimumCPU: "0.1",
},
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(meta)
}
Step 3: Create the Main Entry Point
Create go/my-extension/cmd/my-extension/main.go:
package main
import (
"fmt"
"os"
"github.com/your-org/my-extension/internal/metadata"
)
var Version = "dev"
func main() {
// Handle special commands FIRST
if len(os.Args) > 1 {
switch os.Args[1] {
case "extension-meta":
metadata.Print(Version)
os.Exit(0)
case "version":
fmt.Printf("my-extension version %s\n", Version)
os.Exit(0)
case "help", "--help", "-h":
printHelp()
os.Exit(0)
}
}
// Your command logic here
if len(os.Args) < 2 {
fmt.Println("Usage: my-extension <command>")
os.Exit(1)
}
// Handle commands
switch os.Args[1] {
case "greet":
fmt.Println("Hello from my-extension!")
default:
fmt.Printf("Unknown command: %s\n", os.Args[1])
os.Exit(1)
}
}
func printHelp() {
fmt.Println("my-extension - My custom extension")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" greet Say hello")
fmt.Println(" version Show version")
fmt.Println(" extension-meta Show metadata")
fmt.Println(" help Show this help")
}
Step 4: Initialize Go Module
Step 5: Create Dockerfile
Create containers/my-extension/Dockerfile:
# Multi-stage build for minimal image size
FROM golang:1.21-alpine AS builder
WORKDIR /build
# Install build dependencies
RUN apk add --no-cache git
# Copy dependency files first (caching)
COPY go/my-extension/go.mod go/my-extension/go.sum ./
RUN go mod download
# Copy source code
COPY go/my-extension/ ./
# Build with version injection
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X main.Version=${VERSION}" \
-o my-extension \
./cmd/my-extension
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /workspace
COPY --from=builder /build/my-extension /usr/local/bin/my-extension
ENTRYPOINT ["my-extension"]
CMD ["--help"]
Step 6: Build and Test
Build the Docker image:
cd ../.. # Back to project root
docker build -f containers/my-extension/Dockerfile -t my-extension:dev .
Test the container:
# Test metadata
docker run --rm my-extension:dev extension-meta
# Test help
docker run --rm my-extension:dev --help
# Test your command
docker run --rm my-extension:dev greet
Step 7: Configure for r2r
Create .r2r/r2r-cli.local.yml:
version: "1.0"
extensions:
- name: my-ext
image: my-extension:dev
load_local: true
image_pull_policy: Never
Step 8: Use via r2r CLI
Test your extension through r2r:
What You Learned
Congratulations! You've successfully:
- ✅ Created a proper Go project structure for extensions
- ✅ Implemented required extension metadata
- ✅ Built a multi-stage Docker image
- ✅ Tested the container locally
- ✅ Configured r2r to use your extension
- ✅ Invoked your extension via r2r CLI
Key Concepts Covered
- Extension metadata - Required
extension-metacommand for discovery - Project structure -
cmd/andinternal/organization - Docker multi-stage builds - Optimized container images
- Local development - Building and testing without publishing
- r2r integration - Configuration and invocation
Next Steps
Enhance Your Extension
Now that you have a working extension, you can:
- Add more commands - Extend the switch statement in main.go
- Add business logic - Implement in
internal/core/ - Add output formatting - Support text, JSON, etc.
- Add tests - Write table-driven unit tests
- Add specifications - Write Gherkin feature files
Learn Production Patterns
For production-ready extensions, see:
- Creating Extensions Guide - Complete patterns:
- Testing strategies (unit, behavior, container)
- EAC integration for automated builds
- Publishing to registries
- Multi-platform builds
-
Release workflows
-
ext-env-check Example - Study a real extension:
- Production project structure
- Comprehensive testing
- CI/CD workflows
- Changelog-based releases
Related Guides
- Local Development Workflows - Iterate faster
- Testing in External Repos - Validate across projects
Tutorials | How-to Guides | Explanation | Reference
You are here: Tutorials — learning-oriented guides that take you through steps to complete a project.