Plugin System Architecture
Sortie uses a plugin architecture for extensible connectors, allowing new functionality to be added without modifying core code.
Overview
The plugin system supports three types of plugins:
- Launcher Plugins - Handle different application launch mechanisms
- Auth Plugins - Handle authentication and authorization
- Storage Plugins - Handle data persistence
Quick Start
Configuration
Configure plugins via environment variables:
# Select plugins (defaults shown)
export SORTIE_PLUGIN_LAUNCHER=url # or: container
export SORTIE_PLUGIN_AUTH=noop # default, no auth
export SORTIE_PLUGIN_STORAGE=sqlite # or: memoryUsing the Plugin Registry
import (
"context"
"github.com/rjsadow/sortie/internal/plugins"
_ "github.com/rjsadow/sortie/internal/plugins/launcher"
_ "github.com/rjsadow/sortie/internal/plugins/auth"
_ "github.com/rjsadow/sortie/internal/plugins/storage"
)
func main() {
ctx := context.Background()
// Load configuration
cfg := plugins.LoadRegistryConfig()
// Initialize registry
registry := plugins.Global()
if err := registry.Initialize(ctx, cfg); err != nil {
log.Fatal(err)
}
defer registry.Close()
// Use plugins
launcher := registry.Launcher()
auth := registry.Auth()
storage := registry.Storage()
}Built-in Plugins
Launcher Plugins
URL Launcher (url)
Simple redirect-based launching where users are directed to the application URL.
export SORTIE_PLUGIN_LAUNCHER=urlContainer Launcher (container)
Kubernetes container-based launching with VNC sidecar for interactive desktop applications.
export SORTIE_PLUGIN_LAUNCHER=container
# Optional configuration
export SORTIE_NAMESPACE=default
export KUBECONFIG=/path/to/kubeconfig
export SORTIE_VNC_SIDECAR_IMAGE=theasp/novnc:latestAuth Plugins
Noop Auth (noop)
No-operation auth provider that allows all access. Suitable for development and trusted environments.
export SORTIE_PLUGIN_AUTH=noopStorage Plugins
SQLite Storage (sqlite)
SQLite database storage provider. Default and recommended for production.
export SORTIE_PLUGIN_STORAGE=sqlite
export SORTIE_DB=sortie.dbMemory Storage (memory)
In-memory storage for testing and development. Data is lost on restart.
export SORTIE_PLUGIN_STORAGE=memoryCreating Custom Plugins
Step 1: Implement the Interface
Choose the appropriate interface based on your plugin type:
// For launcher plugins
type LauncherPlugin interface {
Plugin
SupportedTypes() []LaunchType
Launch(ctx context.Context, req *LaunchRequest) (*LaunchResult, error)
GetStatus(ctx context.Context, sessionID string) (*LaunchResult, error)
Terminate(ctx context.Context, sessionID string) error
ListSessions(ctx context.Context, userID string) ([]*LaunchResult, error)
}
// For auth plugins
type AuthProvider interface {
Plugin
Authenticate(ctx context.Context, token string) (*AuthResult, error)
GetUser(ctx context.Context, userID string) (*User, error)
HasPermission(ctx context.Context, userID, permission string) (bool, error)
GetLoginURL(redirectURL string) string
HandleCallback(ctx context.Context, code, state string) (*AuthResult, error)
Logout(ctx context.Context, token string) error
}
// For storage plugins
type StorageProvider interface {
Plugin
CreateApp(ctx context.Context, app *Application) error
GetApp(ctx context.Context, id string) (*Application, error)
UpdateApp(ctx context.Context, app *Application) error
DeleteApp(ctx context.Context, id string) error
ListApps(ctx context.Context) ([]*Application, error)
// ... session and audit methods
}Step 2: Register the Plugin
Register your plugin in an init() function:
package myplugin
import "github.com/rjsadow/sortie/internal/plugins"
func init() {
plugins.RegisterGlobal(plugins.PluginTypeLauncher, "myplugin", func() plugins.Plugin {
return NewMyPlugin()
})
}Step 3: Import the Plugin
Import your plugin package to register it:
import _ "github.com/myorg/myplugin"Example: OIDC Auth Plugin
Here's a skeleton for an OpenID Connect authentication plugin:
package oidc
import (
"context"
"github.com/rjsadow/sortie/internal/plugins"
)
type OIDCAuthProvider struct {
issuer string
clientID string
clientSecret string
redirectURL string
}
func init() {
plugins.RegisterGlobal(plugins.PluginTypeAuth, "oidc", func() plugins.Plugin {
return &OIDCAuthProvider{}
})
}
func (p *OIDCAuthProvider) Name() string { return "oidc" }
func (p *OIDCAuthProvider) Type() plugins.PluginType { return plugins.PluginTypeAuth }
func (p *OIDCAuthProvider) Version() string { return "1.0.0" }
func (p *OIDCAuthProvider) Description() string {
return "OpenID Connect authentication provider"
}
func (p *OIDCAuthProvider) Initialize(ctx context.Context, config map[string]string) error {
p.issuer = config["issuer"]
p.clientID = config["client_id"]
p.clientSecret = config["client_secret"]
p.redirectURL = config["redirect_url"]
// Initialize OIDC client...
return nil
}
func (p *OIDCAuthProvider) Authenticate(ctx context.Context, token string) (*plugins.AuthResult, error) {
// Validate token with OIDC provider...
return &plugins.AuthResult{Authenticated: true}, nil
}
// ... implement remaining methodsPlugin Configuration
Plugins receive configuration through the Initialize method as a map[string]string. Configuration can be set via:
- Environment variables - Loaded by
LoadRegistryConfig() - Config files - Passed through
RegistryConfig.PluginConfigs - Programmatic configuration - Set directly on
RegistryConfig
Example Configuration Structure
cfg := &plugins.RegistryConfig{
Launcher: "container",
Auth: "oidc",
Storage: "sqlite",
PluginConfigs: map[string]map[string]string{
"launcher.container": {
"namespace": "sortie",
"session_timeout": "2h",
"pod_ready_timeout": "5m",
},
"auth.oidc": {
"issuer": "https://auth.example.com",
"client_id": "sortie",
"client_secret": "secret",
},
"storage.sqlite": {
"db_path": "/data/sortie.db",
},
},
}Health Checks
All plugins implement health checking:
// Check individual plugin
if !registry.Launcher().Healthy(ctx) {
log.Warn("Launcher unhealthy")
}
// Check all plugins
statuses := registry.HealthCheck(ctx)
for _, status := range statuses {
log.Printf("%s.%s: healthy=%v", status.PluginType, status.PluginName, status.Healthy)
}Plugin Discovery
List all registered plugins:
// List all plugins
plugins := registry.ListPlugins(ctx)
for _, p := range plugins {
fmt.Printf("%s: %s v%s - %s\n", p.Type, p.Name, p.Version, p.Description)
}
// List by type
launchers := registry.ListPluginsByType(plugins.PluginTypeLauncher)Best Practices
- Thread Safety - Plugins may be called concurrently; use appropriate synchronization
- Context Propagation - Respect context cancellation and timeouts
- Error Handling - Return meaningful errors using the standard error types in
plugins.Err* - Configuration Validation - Validate configuration in
Initialize()and fail fast - Resource Cleanup - Implement
Close()to release resources properly - Health Checks - Implement meaningful
Healthy()checks
Error Handling
Use standard error types for consistency:
var (
ErrPluginNotFound // Plugin not registered
ErrPluginNotReady // Plugin not initialized
ErrInvalidConfig // Invalid configuration
ErrOperationFailed // Generic operation failure
ErrNotImplemented // Operation not supported
ErrAuthRequired // Authentication required
ErrPermissionDenied // Permission denied
ErrResourceNotFound // Resource not found
ErrResourceExists // Resource already exists
ErrConnectionFailed // Connection failed
ErrTimeout // Operation timed out
)