Skip to main content

Plugin System Architecture

The Flyte plugin system allows you to extend the platform's execution capabilities by mapping specific task types (like python, container, or spark) to specialized execution logic. This architecture ensures that the core Flyte engine remains agnostic of the specific details of how a task runs, whether it's a simple Go function or a complex Kubernetes resource.

Plugin Registration

To add a new task type to Flyte, you register a plugin using a PluginEntry. This entry acts as a manifest that tells Flyte which task types the plugin handles and how to initialize it.

Registration typically happens in an init() block within the plugin's package. For example, the core-sleep plugin in flyteplugins/go/tasks/plugins/core/sleep/plugin.go registers itself like this:

func init() {
pluginmachinery.PluginRegistry().RegisterCorePlugin(
core.PluginEntry{
ID: "core-sleep",
RegisteredTaskTypes: []core.TaskType{"core-sleep"},
LoadPlugin: func(ctx context.Context, iCtx core.SetupContext) (core.Plugin, error) {
return &Plugin{
taskStartTimes: make(map[string]time.Time),
}, nil
},
IsDefault: false,
},
)
}

The PluginEntry struct (defined in flyteplugins/go/tasks/pluginmachinery/core/plugin.go) contains:

  • ID: A unique identifier for the plugin.
  • RegisteredTaskTypes: A list of task types this plugin is responsible for.
  • LoadPlugin: A lazy-loading function called by the registry to initialize the plugin instance.
  • IsDefault: If true, this plugin will be used for any task type that doesn't have a specific mapping.

The Plugin Interface

Every plugin must implement the Plugin interface found in flyteplugins/go/tasks/pluginmachinery/core/plugin.go. This interface defines the lifecycle of a task execution:

type Plugin interface {
GetID() string
GetProperties() PluginProperties
Handle(ctx context.Context, tCtx TaskExecutionContext) (Transition, error)
Abort(ctx context.Context, tCtx TaskExecutionContext) error
Finalize(ctx context.Context, tCtx TaskExecutionContext) error
}

Execution Lifecycle

  1. Handle: This is the primary entry point called during every reconciliation loop. It must be idempotent and non-blocking. It returns a Transition indicating the next state of the task (e.g., Running, Success, or Failure).
  2. Abort: Called when a task needs to be stopped (e.g., the workflow was cancelled). Like Handle, it must be idempotent.
  3. Finalize: Always called after Handle or Abort to perform cleanup, such as releasing resources or deleting temporary files.

The Plugin Registry

The Registry class in executor/pkg/plugin/registry.go is the central coordinator that manages these plugins within the Flyte executor. It wraps the global plugin registry and provides a mapping from task types to their respective implementations.

Initialization

During executor startup (typically in executor/setup.go), the Registry is initialized. This process iterates through all registered PluginEntry objects and calls their LoadPlugin functions.

// From executor/pkg/plugin/registry.go
func (r *Registry) Initialize(ctx context.Context) error {
// ...
for _, entry := range r.pluginRegistry.GetCorePlugins() {
plugin, err := pluginsCore.LoadPlugin(ctx, r.setupCtx, entry)
if err != nil {
return fmt.Errorf("failed to load core plugin %s: %w", entry.ID, err)
}
for _, taskType := range entry.RegisteredTaskTypes {
r.plugins[taskType] = plugin
}
// ...
}
r.initialized = true
return nil
}

Plugin Resolution

When the TaskAction controller needs to execute a task, it calls ResolvePlugin to find the correct implementation:

func (r *Registry) ResolvePlugin(taskType string) (pluginsCore.Plugin, error) {
r.mu.RLock()
defer r.mu.RUnlock()

if p, ok := r.plugins[taskType]; ok {
return p, nil
}

if r.defaultPlugin != nil {
return r.defaultPlugin, nil
}

return nil, fmt.Errorf("no plugin registered for task type %q", taskType)
}

Core vs. Kubernetes Plugins

Flyte distinguishes between two primary types of plugins:

  1. Core Plugins: These are implemented entirely in Go and run within the Flyte executor process. The core-sleep plugin is a prime example. They are ideal for lightweight logic or interacting with external APIs.
  2. Kubernetes (K8s) Plugins: These plugins manage Kubernetes resources (like Pods, Jobs, or CRDs). They use a simplified k8s.Plugin interface.

To maintain a consistent interface for the executor, the Registry wraps K8s plugins in a PluginManager (found in executor/pkg/plugin/k8s/plugin_manager.go). The PluginManager adapts the K8s-specific logic into the standard core.Plugin interface, handling resource creation, status polling, and event watching automatically.

Implementation Considerations

  • Idempotency: Because Flyte uses a reconciliation loop, Handle, Abort, and Finalize may be called multiple times for the same task execution. Your implementation must handle this gracefully.
  • Lazy Loading: The LoadPlugin function is only called once during the Registry.Initialize phase. Use this to set up shared resources like clients or caches.
  • Task Type Collisions: If two plugins register for the same task type, the Registry will log a warning and the last one registered will take precedence. Avoid collisions by using unique, descriptive task type strings.