Skip to content

Framework: Introduce the new Task framework

Hossein Pursultani requested to merge framework-tasks into main

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

Merge request reports

Loading