Framework: Introduce the new Task framework
What does this MR do?
The new framework provides a new interface for Task that follows the procedural model. The idea is that Tasks use the context to pass information instead of returning values. This allows for more flexible orchestration of Tasks where the parameters are registered and recalled from the context rather that explicit values.
In addition to the basic Task interface to additional Action and Middleware interfaces are provided to allow composition of Tasks and reusing the building blocks to assemble new Tasks rapidly.
Examples
The following examples put the abstract design of the Task framework into perspective.
type Condition interface {
Evaluate(*RuntimeContext) (bool, error)
}
type AlwaysTrue struct{}
func (c *AlwaysTrue) Evaluate(rtCtx *RuntimeContext) (bool, error) {
return true, nil
}
// Guard is a Middleware that aborts the Task when condition is false.
type Guard struct {
Condition Condition
}
func (m *Guard) Handle(rtCtx *RuntimeContext, value any, err error) (any, error) {
outcome, err := m.Condition.Evaluate(rtCtx)
if err != nil {
return value, err
}
if !outcome {
return value, ErrAbortTask
}
return value, nil
}
// Logger is a Middleware that logs a message at a given level.
type Logger struct {
Level slog.Level
Message string
}
func (m *Logger) Handle(rtCtx *RuntimeContext, value any, err error) (any, error) {
if m.Message != "" {
rtCtx.Logger.Log(rtCtx, m.Level, m.Message)
}
return value, nil
}
// Register is a Middleware that stores its input value in the runtime context
// using the given key and tags.
type Register struct {
Key string
Tags []string
}
func (m *Register) Handle(rtCtx *RuntimeContext, value any, err error) (any, error) {
if m.Key != "" {
rtCtx.State.Store(m.Key, value, m.Tags...)
}
return value, nil
}
// Apply is a Task that applies objects.
type Apply struct {
Guard
Register
Logger
Objects ContextualValue[client.Object]
SetOwnerReference bool
OwnerIsController bool
Owner ContextualValue[client.Object]
}
func (t *Apply) Handle(rtCtx *RuntimeContext, value any, err error) (any, error) {
objects := []client.Object{}
// Apply stuff ...
return objects, nil
}
func (t *Apply) Execute(rtCtx *RuntimeContext) error {
// 1. Check the condition
// 2. Log a message
// 3. Run apply function
// 4. Register the result
_, err := MiddlewareChain{&t.Guard, &t.Logger, t, &t.Register}.Handle(rtCtx, nil, nil)
return err
}
// Create is a Task that creates objects.
type Create struct {
Guard
Register
Logger
Objects ContextualValue[client.Object]
IgnoreAlreadyExistsError bool
}
Author's Checklist
For anything in this list which will not be completed, please provide a reason in the MR discussion.
Required
-
Ensure a release milestone is set. -
MR title and description are up to date, accurate, and descriptive. -
MR targeting the appropriate branch. -
MR has a green pipeline on GitLab.com. -
When ready for review, MR is labeled workflowready for review per the MR workflow.
Expected
-
Test plan indicating conditions for success has been posted and passes. -
Documentation is created or updated. -
Tests are added.
Related issues
Related to #66 (closed)
Edited by Hossein Pursultani