~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
113 lines · 3.5 KB Raw History
package controller

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	policyv1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/policy/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/executor"
	policyengine "github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/status"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/workflow"
)

type WorkflowReconciler struct {
	Actions workflow.ActionResolver

	Records RecordSource
}

func (r *WorkflowReconciler) ReconcilePolicyWorkflow(ctx context.Context, workflowCR *policyv1alpha1.PolicyWorkflow) (WorkflowReconcileState, error) {
	if workflowCR == nil {
		return WorkflowReconcileState{}, fmt.Errorf("policy workflow is required")
	}
	if workflowCR.DeletionTimestamp != nil {
		state := WorkflowReconcileState{Phase: policyv1alpha1.PolicyWorkflowPending}
		meta.SetStatusCondition(&state.Conditions, metav1.Condition{
			Type:    policyengine.ConditionReady,
			Status:  metav1.ConditionFalse,
			Reason:  "Terminating",
			Message: "the policy workflow is being deleted",
		})
		return state, nil
	}
	var raw json.RawMessage
	if workflowCR.Spec.RecordRef != nil {
		if r.Records == nil {
			return workflowInvalid("the policy workflow record resolver is not configured"), nil
		}
		resolved, err := r.Records.ResolveWorkflow(ctx, *workflowCR.Spec.RecordRef)
		if err != nil {
			if ctx.Err() != nil {
				return WorkflowReconcileState{Phase: policyv1alpha1.PolicyWorkflowPending}, ctx.Err()
			}
			return workflowInvalid(err.Error()), err
		}
		raw = resolved
	} else {
		if len(bytes.TrimSpace(workflowCR.Spec.Workflow)) == 0 {
			if bundled, ok := bundledWorkflow(workflowCR.Name); ok {
				return r.validate(bundled), nil
			}
			return workflowInvalid("workflow or recordRef is required"), nil
		}
		raw = workflowCR.Spec.Workflow
	}
	return r.validate(raw), nil
}

func (r *WorkflowReconciler) validate(raw json.RawMessage) WorkflowReconcileState {
	parsed, err := workflow.Parse(raw)
	if err != nil {
		return workflowInvalid(err.Error())
	}
	if job, ok := workflow.HasRunStep(raw); ok {
		return workflowInvalid(fmt.Sprintf("%s a run: step (job %s)", executor.NetOnlyRefused, job))
	}
	resolver := r.Actions
	if resolver == nil {
		resolver = executor.DefaultActionResolver()
	}
	for _, job := range parsed.Jobs {
		for _, step := range job.Steps {
			if step.Uses == "" {
				continue
			}
			action, actionErr := resolver(step.Uses)
			if actionErr != nil {
				return workflowInvalid(actionErr.Error())
			}
			if !strings.HasPrefix(action.Using, "node") {
				return workflowInvalid(fmt.Sprintf("action %s uses unsupported runtime %s", step.Uses, action.Using))
			}
		}
	}
	return workflowReady("Valid", "the gha-lite workflow is valid")
}

func workflowReady(reason, message string) WorkflowReconcileState {
	state := WorkflowReconcileState{Phase: policyv1alpha1.PolicyWorkflowReady}
	meta.SetStatusCondition(&state.Conditions, metav1.Condition{
		Type:    policyengine.ConditionReady,
		Status:  metav1.ConditionTrue,
		Reason:  reason,
		Message: message,
	})
	return state
}

func workflowInvalid(message string) WorkflowReconcileState {
	state := WorkflowReconcileState{Phase: policyv1alpha1.PolicyWorkflowInvalid}
	meta.SetStatusCondition(&state.Conditions, metav1.Condition{
		Type:    policyengine.ConditionReady,
		Status:  metav1.ConditionFalse,
		Reason:  "Invalid",
		Message: message,
	})
	return state
}