blob: 02de8092bc812a5d7d5eed92b0322ba67b72dea7 [file] [log] [blame]
// Copyright 2023 Red Hat, Inc. and/or its affiliates
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package profiles
import (
"context"
"fmt"
"k8s.io/klog/v2"
"k8s.io/client-go/rest"
"github.com/kiegroup/kogito-serverless-operator/api/metadata"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
operatorapi "github.com/kiegroup/kogito-serverless-operator/api/v1alpha08"
"github.com/kiegroup/kogito-serverless-operator/log"
)
// ProfileReconciler is the public interface to have access to this package and perform the actual reconciliation flow.
//
// There are a few concepts in this package that you need to understand before attempting to maintain it:
//
// 1. ProfileReconciler: it's the main interface that internal structs implement via the baseReconciler.
// Every profile must embed the baseReconciler.
//
// 2. stateSupport: is a struct with a few support objects passed around the reconciliation states like the client and logger.
//
// 3. reconciliationStateMachine: is a struct within the ProfileReconciler that do the actual reconciliation.
// Each part of the reconciliation algorithm is a ReconciliationState that will be executed based on the ReconciliationState.CanReconcile call.
//
// 4. ReconciliationState: is where your business code should be focused on. Each state should react to a specific operatorapi.SonataFlowConditionType.
// The least conditions your state handles, the better.
// The ReconciliationState can provide specific code that will only be triggered if the workflow is in that specific condition.
//
// 5. objectCreator: are functions to create a specific Kubernetes object based on a given workflow instance. This function should return the desired default state.
//
// 6. mutateVisitor: is a function that states can pass to defaultObjectEnsurer that will be applied to a given live object during the reconciliation cycle.
// For example, if you wish to guarantee that an image in a specific container in the Deployment that you control and own won't change, make sure that your
// mutate function guarantees that.
//
// 7. defaultObjectEnsurer: is a struct for a given objectCreator to control the reconciliation and merge conditions to an object.
// A ReconciliationState may or may not have one or more ensurers. Depends on their role. There are states that just read objects, so no need to keep their desired state.
//
// See the already implemented reconciliation profiles to have a better understanding.
//
// While debugging, focus on the ReconciliationState(s), not in the profile implementation since the base algorithm is the same for every profile.
type ProfileReconciler interface {
Reconcile(ctx context.Context, workflow *operatorapi.SonataFlow) (ctrl.Result, error)
GetProfile() metadata.ProfileType
}
// stateSupport is the shared structure with common accessors used throughout the whole reconciliation profiles
type stateSupport struct {
client client.Client
}
// performStatusUpdate updates the SonataFlow Status conditions
func (s stateSupport) performStatusUpdate(ctx context.Context, workflow *operatorapi.SonataFlow) (bool, error) {
var err error
workflow.Status.ObservedGeneration = workflow.Generation
if err = s.client.Status().Update(ctx, workflow); err != nil {
klog.V(log.E).ErrorS(err, "Failed to update Workflow status")
return false, err
}
return true, err
}
// PostReconcile function to perform all the other operations required after the reconciliation - placeholder for null pattern usages
func (s stateSupport) PostReconcile(ctx context.Context, workflow *operatorapi.SonataFlow) error {
//By default, we don't want to perform anything after the reconciliation, and so we will simply return no error
return nil
}
// baseReconciler is the base structure used by every reconciliation profile.
// Use newBaseProfileReconciler to build a new reference.
type baseReconciler struct {
*stateSupport
reconciliationStateMachine *reconciliationStateMachine
objects []client.Object
}
func newBaseProfileReconciler(support *stateSupport, stateMachine *reconciliationStateMachine) baseReconciler {
return baseReconciler{
stateSupport: support,
reconciliationStateMachine: stateMachine,
}
}
// Reconcile does the actual reconciliation algorithm based on a set of ReconciliationState
func (b baseReconciler) Reconcile(ctx context.Context, workflow *operatorapi.SonataFlow) (ctrl.Result, error) {
workflow.Status.Manager().InitializeConditions()
result, objects, err := b.reconciliationStateMachine.do(ctx, workflow)
if err != nil {
return result, err
}
b.objects = objects
klog.V(log.I).InfoS("Returning from reconciliation", "Result", result)
return result, err
}
// ReconciliationState is an interface implemented internally by different reconciliation algorithms to perform the adequate logic for a given workflow profile
type ReconciliationState interface {
// CanReconcile checks if this state can perform its reconciliation task
CanReconcile(workflow *operatorapi.SonataFlow) bool
// Do perform the reconciliation task. It returns the controller result, the objects updated, and an error if any.
// Objects can be nil if the reconciliation state doesn't perform any updates in any Kubernetes object.
Do(ctx context.Context, workflow *operatorapi.SonataFlow) (ctrl.Result, []client.Object, error)
// PostReconcile performs the actions to perform after the reconciliation that are not mandatory
PostReconcile(ctx context.Context, workflow *operatorapi.SonataFlow) error
}
// newReconciliationStateMachine builder for the reconciliationStateMachine
func newReconciliationStateMachine(states ...ReconciliationState) *reconciliationStateMachine {
return &reconciliationStateMachine{
states: states,
}
}
// reconciliationStateMachine implements (sort of) the command pattern and delegate to a chain of ReconciliationState
// the actual task to reconcile in a given workflow condition
//
// TODO: implement state transition, so based on a given condition we do the status update which actively transition the object state
type reconciliationStateMachine struct {
states []ReconciliationState
}
func (r *reconciliationStateMachine) do(ctx context.Context, workflow *operatorapi.SonataFlow) (ctrl.Result, []client.Object, error) {
for _, h := range r.states {
if h.CanReconcile(workflow) {
klog.V(log.I).InfoS("Found a condition to reconcile.", "Conditions", workflow.Status.Conditions)
result, objs, err := h.Do(ctx, workflow)
if err != nil {
return result, objs, err
}
if err = h.PostReconcile(ctx, workflow); err != nil {
klog.V(log.E).ErrorS(err, "Error in Post Reconcile actions.", "Workflow", workflow.Name, "Conditions", workflow.Status.Conditions)
}
return result, objs, err
}
}
return ctrl.Result{}, nil, fmt.Errorf("the workflow %s in the namespace %s is in an unknown state condition. Can't reconcilie. Status is: %v", workflow.Name, workflow.Namespace, workflow.Status)
}
// NewReconciler creates a new ProfileReconciler based on the given workflow and context.
func NewReconciler(client client.Client, config *rest.Config, workflow *operatorapi.SonataFlow) ProfileReconciler {
return profileBuilder(workflow)(client, config)
}