Plugin Development
Plugin Development
Section titled “Plugin Development”Forkspacer supports a plugin system that allows you to extend the operator with custom resource management logic. Plugins enable you to integrate third-party tools, implement custom deployment strategies, or handle specialized workloads beyond the built-in Helm and Kubernetes manifest support.
What are Plugins?
Section titled “What are Plugins?”Plugins are Go shared libraries (.so
files) that implement the IManager
interface. They are dynamically loaded by the operator at runtime and provide custom lifecycle management for Module resources.
Plugin Capabilities
Section titled “Plugin Capabilities”A plugin must implement four core lifecycle methods:
- Install: Deploy and configure resources
- Uninstall: Remove resources and clean up
- Sleep: Hibernate or scale down resources
- Resume: Wake up or scale up resources
Plugin Architecture
Section titled “Plugin Architecture”Interface Definition
Section titled “Interface Definition”All plugins must implement the base.IManager
interface:
type IManager interface { Install(ctx context.Context, metaData MetaData) error Uninstall(ctx context.Context, metaData MetaData) error Sleep(ctx context.Context, metaData MetaData) error Resume(ctx context.Context, metaData MetaData) error}
Plugin Factory Function
Section titled “Plugin Factory Function”Each plugin must export a NewManager
function with this exact signature:
func NewManager( ctx context.Context, logger logr.Logger, kubernetesConfig *rest.Config, config map[string]any,) (base.IManager, error)
This function is called when the operator loads your plugin and should return an instance that implements IManager
.
Creating a Plugin
Section titled “Creating a Plugin”Step 1: Create Plugin Directory
Section titled “Step 1: Create Plugin Directory”Create a directory for your plugin under plugins/
:
mkdir -p plugins/my-plugin
Step 2: Write Plugin Code
Section titled “Step 2: Write Plugin Code”Create plugins/my-plugin/main.go
:
package main
import ( "context" "fmt"
"github.com/go-logr/logr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest"
"github.com/forkspacer/forkspacer/pkg/manager/base")
// Ensure your plugin implements the required factory function typevar _ base.NewCustomManagerT = NewManager
// NewManager is the entry point for the pluginfunc NewManager( ctx context.Context, logger logr.Logger, kubernetesConfig *rest.Config, config map[string]any,) (base.IManager, error) { // Initialize Kubernetes client kubernetesClient, err := kubernetes.NewForConfig(kubernetesConfig) if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) }
// Parse plugin configuration appName, ok := config["appName"].(string) if !ok || appName == "" { appName = "default-app" }
return &MyPlugin{ log: logger, kubernetesClient: kubernetesClient, appName: appName, }, nil}
// MyPlugin implements the IManager interfacetype MyPlugin struct { log logr.Logger kubernetesClient *kubernetes.Clientset appName string}
func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { p.log.Info("Installing custom application", "app", p.appName)
// Your custom installation logic here // - Create deployments, services, configmaps, etc. // - Store state in metaData if needed for later operations
return nil}
func (p *MyPlugin) Uninstall(ctx context.Context, metaData base.MetaData) error { p.log.Info("Uninstalling custom application", "app", p.appName)
// Your custom uninstallation logic here // - Delete resources created during Install // - Clean up any external resources
return nil}
func (p *MyPlugin) Sleep(ctx context.Context, metaData base.MetaData) error { p.log.Info("Hibernating custom application", "app", p.appName)
// Your custom hibernation logic here // - Scale down deployments // - Store current state in metaData // - Suspend resources
return nil}
func (p *MyPlugin) Resume(ctx context.Context, metaData base.MetaData) error { p.log.Info("Resuming custom application", "app", p.appName)
// Your custom resume logic here // - Restore state from metaData // - Scale up deployments // - Resume suspended resources
return nil}
// Required: empty main function for plugin buildfunc main() {}
Step 3: Build the Plugin
Section titled “Step 3: Build the Plugin”Use the Makefile target to build your plugin:
make build-plugin PLUGIN=my-plugin
This command:
- Builds the plugin in a Docker container with the same Go version as the operator
- Compiles the plugin as a shared library (
.so
) - Extracts the plugin to
plugins/my-plugin/plugin.so
Output:
Building plugin 'my-plugin' in Docker container...Plugin built successfully: plugins/my-plugin/plugin.soPlugin size: 15M
Using Your Plugin
Section titled “Using Your Plugin”Option 1: From HTTP URL
Section titled “Option 1: From HTTP URL”Host your plugin file and reference it via HTTP:
apiVersion: batch.forkspacer.com/v1kind: Modulemetadata: name: my-custom-app namespace: defaultspec: workspace: name: dev-workspace namespace: default
source: raw: kind: Custom spec: repo: file: "https://example.com/plugins/my-plugin.so"
Option 2: From ConfigMap
Section titled “Option 2: From ConfigMap”Store the plugin in a ConfigMap and reference it:
- Create ConfigMap with plugin binary:
kubectl create configmap my-plugin-cm \ --from-file=plugin=plugins/my-plugin/plugin.so \ -n default
- Reference in Module:
apiVersion: batch.forkspacer.com/v1kind: Modulemetadata: name: my-custom-app namespace: defaultspec: workspace: name: dev-workspace namespace: default
source: raw: kind: Custom spec: repo: configMap: name: my-plugin-cm namespace: default
Working with MetaData
Section titled “Working with MetaData”The MetaData
parameter in each method is a persistent key-value store that survives across reconciliation cycles. Use it to store state information.
Storing Data
Section titled “Storing Data”func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { // Store deployment name for later use deploymentName := "my-app-deployment" metaData["deploymentName"] = deploymentName
// Store complex data as JSON-serializable maps metaData["installedResources"] = map[string]string{ "deployment": deploymentName, "service": "my-app-service", }
return nil}
Retrieving Data
Section titled “Retrieving Data”func (p *MyPlugin) Uninstall(ctx context.Context, metaData base.MetaData) error { // Retrieve simple string deploymentName := metaData.DecodeToString("deploymentName")
// Retrieve complex data var resources map[string]string if err := mapstructure.Decode(metaData["installedResources"], &resources); err != nil { return fmt.Errorf("failed to decode resources: %w", err) }
// Use the data p.log.Info("Deleting deployment", "name", deploymentName)
return nil}
Best Practices
Section titled “Best Practices”1. Error Handling
Section titled “1. Error Handling”Always return descriptive errors:
func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { deployment, err := p.createDeployment(ctx) if err != nil { return fmt.Errorf("failed to create deployment: %w", err) }
metaData["deploymentName"] = deployment.Name return nil}
2. Idempotency
Section titled “2. Idempotency”Make all operations idempotent:
func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { // Check if already installed deploymentName := metaData.DecodeToString("deploymentName") if deploymentName != "" { p.log.Info("Deployment already exists", "name", deploymentName) return nil }
// Create deployment // ...}
3. Context Awareness
Section titled “3. Context Awareness”Respect context cancellation:
func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { select { case <-ctx.Done(): return ctx.Err() default: // Proceed with installation }
// For long-running operations, check context periodically}
4. Structured Logging
Section titled “4. Structured Logging”Use structured logging with the provided logger:
func (p *MyPlugin) Install(ctx context.Context, metaData base.MetaData) error { p.log.Info("Starting installation", "app", p.appName, "namespace", p.namespace, )
// ...
p.log.Info("Installation completed", "app", p.appName, "deploymentName", deploymentName, )}
5. Resource Cleanup
Section titled “5. Resource Cleanup”Always clean up resources in Uninstall:
func (p *MyPlugin) Uninstall(ctx context.Context, metaData base.MetaData) error { // Get all resources created during Install resources := metaData["installedResources"].(map[string]string)
// Delete in reverse order of creation if svcName, ok := resources["service"]; ok { if err := p.deleteService(ctx, svcName); err != nil { return fmt.Errorf("failed to delete service: %w", err) } }
if depName, ok := resources["deployment"]; ok { if err := p.deleteDeployment(ctx, depName); err != nil { return fmt.Errorf("failed to delete deployment: %w", err) } }
return nil}
Next Steps
Section titled “Next Steps”- Development Overview - Learn about operator development
- API Reference - Understand Module CRD specification
- Examples - Browse example plugins
Resources
Section titled “Resources”- Go Plugin Package - Official Go plugin documentation
- Kubernetes Client-Go - Kubernetes Go client library
- logr - Structured logging interface