1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
}