From ff33bf5c644e133bfa8a667ea40be736d2fe0e7d Mon Sep 17 00:00:00 2001 From: David Zager Date: Thu, 30 Jul 2026 13:25:56 -0400 Subject: [PATCH] Rename AgentPlaybook to AgentWorkflow; fix stage bugs Rename AgentPlaybook/AgentPlaybookRun to AgentWorkflow/AgentWorkflowRun across CRD types, controllers, tests, CRD manifests, RBAC, docs, and changelog fragments. KONVEYOR_PLAYBOOK_INSTRUCTIONS renamed to KONVEYOR_WORKFLOW_GUIDE. Short names updated: ap->aw, apr->awr. Fix #51: Change AgentRun sandbox restartPolicy from OnFailure to Never so that failed stages reach a terminal phase instead of crashlooping indefinitely. Fix #52: Filter workflow-run params to only those the stage Agent declares. The controller looks up the stage Agent and forwards only matching params, removing the coupling that forced every stage Agent to declare every param from other stages. Add stage metadata env vars (#68): inject KONVEYOR_WORKFLOW_STAGE and KONVEYOR_WORKFLOW_STAGE_COUNT into each child AgentRun. The harness uses these for stage-aware token revocation (revoke only on last stage). Closes #63 Fixes #51 Fixes #52 Refs #68 Signed-off-by: David Zager Signed-off-by: David Zager --- AGENTS.md | 4 +- README.md | 4 +- ...aybook_types.go => agentworkflow_types.go} | 36 +- ...run_types.go => agentworkflowrun_types.go} | 50 +-- api/v1alpha1/groupversion_info.go | 8 +- api/v1alpha1/zz_generated.deepcopy.go | 372 +++++++++--------- .../unreleased/agentplaybook-controllers.yaml | 10 - .../unreleased/agentworkflow-controllers.yaml | 10 + changes/unreleased/scaffold-crds.yaml | 2 +- cmd/main.go | 13 +- ...aml => konveyor.io_agentworkflowruns.yaml} | 44 +-- ...s.yaml => konveyor.io_agentworkflows.yaml} | 26 +- config/crd/kustomization.yaml | 4 +- config/rbac/role.yaml | 19 +- .../0001-agentic-platform-crd-architecture.md | 22 +- ...003-hub-curated-api-for-agent-resources.md | 2 +- ...arness-thin-runner-and-skillcard-skills.md | 6 +- docs/slides/demo-2026-07-16.md | 16 +- hack/harness-test/resources.yaml | 2 +- ...resources.yaml => workflow-resources.yaml} | 10 +- harness/README.md | 4 +- harness/internal/config/config.go | 6 +- harness/internal/config/config_test.go | 4 +- internal/controller/agentrun_controller.go | 8 +- .../controller/agentrun_controller_test.go | 3 + ...troller.go => agentworkflow_controller.go} | 86 ++-- ...st.go => agentworkflow_controller_test.go} | 40 +- ...ller.go => agentworkflowrun_controller.go} | 242 +++++++----- ...go => agentworkflowrun_controller_test.go} | 238 ++++++++--- internal/controller/crd_validation_test.go | 55 +-- internal/controller/doc.go | 6 +- internal/controller/suite_test.go | 9 +- 32 files changed, 788 insertions(+), 573 deletions(-) rename api/v1alpha1/{agentplaybook_types.go => agentworkflow_types.go} (76%) rename api/v1alpha1/{agentplaybookrun_types.go => agentworkflowrun_types.go} (72%) delete mode 100644 changes/unreleased/agentplaybook-controllers.yaml create mode 100644 changes/unreleased/agentworkflow-controllers.yaml rename config/crd/bases/{konveyor.io_agentplaybookruns.yaml => konveyor.io_agentworkflowruns.yaml} (95%) rename config/crd/bases/{konveyor.io_agentplaybooks.yaml => konveyor.io_agentworkflows.yaml} (92%) rename hack/harness-test/{playbook-resources.yaml => workflow-resources.yaml} (94%) rename internal/controller/{agentplaybook_controller.go => agentworkflow_controller.go} (60%) rename internal/controller/{agentplaybook_controller_test.go => agentworkflow_controller_test.go} (76%) rename internal/controller/{agentplaybookrun_controller.go => agentworkflowrun_controller.go} (63%) rename internal/controller/{agentplaybookrun_controller_test.go => agentworkflowrun_controller_test.go} (58%) diff --git a/AGENTS.md b/AGENTS.md index f621f859..885c3605 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ All agent-facing guidance for this repository. Read this file first. ## What this project is A Kubernetes controller for managing AI agent workloads. Defines CRDs -(Agent, AgentRun, AgentPlaybook, SkillCard, SkillCollection, -LLMProvider) and controllers for composing and executing agent +(Agent, AgentRun, AgentWorkflow, AgentWorkflowRun, SkillCard, +SkillCollection, LLMProvider) and controllers for composing and executing agent workloads via Agent Sandbox. ## Key documents diff --git a/README.md b/README.md index 155397c4..2a65e2bd 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ CI pipeline) resolves application metadata before creating the CR. | **LLMProvider** | LLM service endpoint, credentials, and available models. | | **Agent** | Template declaring available skills, providers, container image, prompt, and typed parameters. | | **AgentRun** | Execute a single Agent with specific values. Creates an Agent Sandbox. | -| **AgentPlaybook** | Ordered sequence of stages, each referencing an Agent. | -| **AgentPlaybookRun** | Execute a playbook. Creates AgentRuns sequentially per stage. | +| **AgentWorkflow** | Ordered sequence of stages, each referencing an Agent. | +| **AgentWorkflowRun** | Execute a workflow. Creates AgentRuns sequentially per stage. | ### Key design decisions diff --git a/api/v1alpha1/agentplaybook_types.go b/api/v1alpha1/agentworkflow_types.go similarity index 76% rename from api/v1alpha1/agentplaybook_types.go rename to api/v1alpha1/agentworkflow_types.go index ca91cab2..4822a2f2 100644 --- a/api/v1alpha1/agentplaybook_types.go +++ b/api/v1alpha1/agentworkflow_types.go @@ -20,10 +20,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// AgentPlaybookStage defines one stage in a playbook. +// AgentWorkflowStage defines one stage in a workflow. // Each stage references an Agent and carries instructions. -type AgentPlaybookStage struct { - // Name is the stage name, unique within the playbook. +type AgentWorkflowStage struct { + // Name is the stage name, unique within the workflow. // Must be a valid Kubernetes label value (lowercase alphanumeric, // hyphens, dots, max 63 chars) since it is used in labels on // child AgentRun resources. @@ -42,8 +42,8 @@ type AgentPlaybookStage struct { Instructions string `json:"instructions,omitempty"` } -// AgentPlaybookSpec defines the desired state of an AgentPlaybook. -type AgentPlaybookSpec struct { +// AgentWorkflowSpec defines the desired state of an AgentWorkflow. +type AgentWorkflowSpec struct { // Guide is a high-level guide providing ambient context for all stages. // Written as a context file in the workspace so each agent understands // where its work fits in the bigger picture. @@ -57,17 +57,17 @@ type AgentPlaybookSpec struct { // +kubebuilder:validation:MinItems=1 // +listType=map // +listMapKey=name - Stages []AgentPlaybookStage `json:"stages"` + Stages []AgentWorkflowStage `json:"stages"` } -// AgentPlaybookStatus defines the observed state of an AgentPlaybook. -type AgentPlaybookStatus struct { +// AgentWorkflowStatus defines the observed state of an AgentWorkflow. +type AgentWorkflowStatus struct { // ObservedGeneration is the most recent generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` // Conditions represent the latest available observations of the - // AgentPlaybook's state. + // AgentWorkflow's state. // +optional // +listType=map // +listMapKey=type @@ -76,27 +76,27 @@ type AgentPlaybookStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=ap +// +kubebuilder:resource:shortName=aw // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// AgentPlaybook is a reusable playbook combining a high-level guide with an +// AgentWorkflow is a reusable workflow combining a high-level guide with an // ordered sequence of stages. Each stage references an Agent and carries -// instructions. An AgentPlaybook is a template — creating one does not +// instructions. An AgentWorkflow is a template — creating one does not // execute anything. -type AgentPlaybook struct { +type AgentWorkflow struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec AgentPlaybookSpec `json:"spec,omitempty"` - Status AgentPlaybookStatus `json:"status,omitempty"` + Spec AgentWorkflowSpec `json:"spec,omitempty"` + Status AgentWorkflowStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// AgentPlaybookList contains a list of AgentPlaybook. -type AgentPlaybookList struct { +// AgentWorkflowList contains a list of AgentWorkflow. +type AgentWorkflowList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []AgentPlaybook `json:"items"` + Items []AgentWorkflow `json:"items"` } diff --git a/api/v1alpha1/agentplaybookrun_types.go b/api/v1alpha1/agentworkflowrun_types.go similarity index 72% rename from api/v1alpha1/agentplaybookrun_types.go rename to api/v1alpha1/agentworkflowrun_types.go index e0addbb8..050a7783 100644 --- a/api/v1alpha1/agentplaybookrun_types.go +++ b/api/v1alpha1/agentworkflowrun_types.go @@ -21,10 +21,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// AgentPlaybookRunStageStatus tracks the status of a single stage within -// a playbook run. -type AgentPlaybookRunStageStatus struct { - // Name is the stage name, matching a stage in the AgentPlaybook. +// AgentWorkflowRunStageStatus tracks the status of a single stage within +// a workflow run. +type AgentWorkflowRunStageStatus struct { + // Name is the stage name, matching a stage in the AgentWorkflow. Name string `json:"name"` // Phase is the current phase of this stage. @@ -35,13 +35,13 @@ type AgentPlaybookRunStageStatus struct { AgentRunName string `json:"agentRunName,omitempty"` } -// AgentPlaybookRunSpec defines the desired state of an AgentPlaybookRun. +// AgentWorkflowRunSpec defines the desired state of an AgentWorkflowRun. // The spec is immutable once created — delete and recreate to change values. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" -type AgentPlaybookRunSpec struct { - // PlaybookRef is the name of the AgentPlaybook CR to execute. +type AgentWorkflowRunSpec struct { + // WorkflowRef is the name of the AgentWorkflow CR to execute. // +kubebuilder:validation:MinLength=1 - PlaybookRef string `json:"playbookRef"` + WorkflowRef string `json:"workflowRef"` // Models selects specific provider/model combinations for all stages. // Individual stages may override these selections in the future. @@ -67,9 +67,9 @@ type AgentPlaybookRunSpec struct { EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` } -// AgentPlaybookRunStatus defines the observed state of an AgentPlaybookRun. -type AgentPlaybookRunStatus struct { - // Phase is the current phase of the overall playbook run. +// AgentWorkflowRunStatus defines the observed state of an AgentWorkflowRun. +type AgentWorkflowRunStatus struct { + // Phase is the current phase of the overall workflow run. // +kubebuilder:default=Pending // +optional Phase AgentRunPhase `json:"phase,omitempty"` @@ -86,18 +86,18 @@ type AgentPlaybookRunStatus struct { // +optional // +listType=map // +listMapKey=name - Stages []AgentPlaybookRunStageStatus `json:"stages,omitempty"` + Stages []AgentWorkflowRunStageStatus `json:"stages,omitempty"` - // StartTime is the time the playbook run started. + // StartTime is the time the workflow run started. // +optional StartTime *metav1.Time `json:"startTime,omitempty"` - // CompletionTime is the time the playbook run finished. + // CompletionTime is the time the workflow run finished. // +optional CompletionTime *metav1.Time `json:"completionTime,omitempty"` // Conditions represent the latest available observations of the - // AgentPlaybookRun's state. + // AgentWorkflowRun's state. // +optional // +listType=map // +listMapKey=type @@ -106,29 +106,29 @@ type AgentPlaybookRunStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=apr -// +kubebuilder:printcolumn:name="Playbook",type=string,JSONPath=`.spec.playbookRef` +// +kubebuilder:resource:shortName=awr +// +kubebuilder:printcolumn:name="Workflow",type=string,JSONPath=`.spec.workflowRef` // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Current Stage",type=string,JSONPath=`.status.currentStage` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// AgentPlaybookRun is a request to execute an AgentPlaybook. It references -// an AgentPlaybook and carries generic parameters. The controller orchestrates +// AgentWorkflowRun is a request to execute an AgentWorkflow. It references +// an AgentWorkflow and carries generic parameters. The controller orchestrates // execution: creates an AgentRun per stage, manages cross-stage handoff via // committed files on a shared target branch. -type AgentPlaybookRun struct { +type AgentWorkflowRun struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec AgentPlaybookRunSpec `json:"spec,omitempty"` - Status AgentPlaybookRunStatus `json:"status,omitempty"` + Spec AgentWorkflowRunSpec `json:"spec,omitempty"` + Status AgentWorkflowRunStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// AgentPlaybookRunList contains a list of AgentPlaybookRun. -type AgentPlaybookRunList struct { +// AgentWorkflowRunList contains a list of AgentWorkflowRun. +type AgentWorkflowRunList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []AgentPlaybookRun `json:"items"` + Items []AgentWorkflowRun `json:"items"` } diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 3c4e5602..7f645c8f 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -41,10 +41,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(GroupVersion, &Agent{}, &AgentList{}, - &AgentPlaybook{}, - &AgentPlaybookList{}, - &AgentPlaybookRun{}, - &AgentPlaybookRunList{}, + &AgentWorkflow{}, + &AgentWorkflowList{}, + &AgentWorkflowRun{}, + &AgentWorkflowRunList{}, &AgentRun{}, &AgentRunList{}, &LLMProvider{}, diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4e7f3724..23c25d47 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -101,7 +101,22 @@ func (in *AgentParam) DeepCopy() *AgentParam { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybook) DeepCopyInto(out *AgentPlaybook) { +func (in *AgentProviderRef) DeepCopyInto(out *AgentProviderRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentProviderRef. +func (in *AgentProviderRef) DeepCopy() *AgentProviderRef { + if in == nil { + return nil + } + out := new(AgentProviderRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRun) DeepCopyInto(out *AgentRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -109,18 +124,18 @@ func (in *AgentPlaybook) DeepCopyInto(out *AgentPlaybook) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybook. -func (in *AgentPlaybook) DeepCopy() *AgentPlaybook { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRun. +func (in *AgentRun) DeepCopy() *AgentRun { if in == nil { return nil } - out := new(AgentPlaybook) + out := new(AgentRun) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybook) DeepCopyObject() runtime.Object { +func (in *AgentRun) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -128,31 +143,31 @@ func (in *AgentPlaybook) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookList) DeepCopyInto(out *AgentPlaybookList) { +func (in *AgentRunList) DeepCopyInto(out *AgentRunList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentPlaybook, len(*in)) + *out = make([]AgentRun, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookList. -func (in *AgentPlaybookList) DeepCopy() *AgentPlaybookList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunList. +func (in *AgentRunList) DeepCopy() *AgentRunList { if in == nil { return nil } - out := new(AgentPlaybookList) + out := new(AgentRunList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookList) DeepCopyObject() runtime.Object { +func (in *AgentRunList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -160,66 +175,37 @@ func (in *AgentPlaybookList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRun) DeepCopyInto(out *AgentPlaybookRun) { +func (in *AgentRunModelSelection) DeepCopyInto(out *AgentRunModelSelection) { *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRun. -func (in *AgentPlaybookRun) DeepCopy() *AgentPlaybookRun { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunModelSelection. +func (in *AgentRunModelSelection) DeepCopy() *AgentRunModelSelection { if in == nil { return nil } - out := new(AgentPlaybookRun) + out := new(AgentRunModelSelection) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookRun) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunList) DeepCopyInto(out *AgentPlaybookRunList) { +func (in *AgentRunParam) DeepCopyInto(out *AgentRunParam) { *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AgentPlaybookRun, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunList. -func (in *AgentPlaybookRunList) DeepCopy() *AgentPlaybookRunList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunParam. +func (in *AgentRunParam) DeepCopy() *AgentRunParam { if in == nil { return nil } - out := new(AgentPlaybookRunList) + out := new(AgentRunParam) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookRunList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunSpec) DeepCopyInto(out *AgentPlaybookRunSpec) { +func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { *out = *in if in.Models != nil { in, out := &in.Models, &out.Models @@ -247,39 +233,19 @@ func (in *AgentPlaybookRunSpec) DeepCopyInto(out *AgentPlaybookRunSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunSpec. -func (in *AgentPlaybookRunSpec) DeepCopy() *AgentPlaybookRunSpec { - if in == nil { - return nil - } - out := new(AgentPlaybookRunSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunStageStatus) DeepCopyInto(out *AgentPlaybookRunStageStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunStageStatus. -func (in *AgentPlaybookRunStageStatus) DeepCopy() *AgentPlaybookRunStageStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunSpec. +func (in *AgentRunSpec) DeepCopy() *AgentRunSpec { if in == nil { return nil } - out := new(AgentPlaybookRunStageStatus) + out := new(AgentRunSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { +func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { *out = *in - if in.Stages != nil { - in, out := &in.Stages, &out.Stages - *out = make([]AgentPlaybookRunStageStatus, len(*in)) - copy(*out, *in) - } if in.StartTime != nil { in, out := &in.StartTime, &out.StartTime *out = (*in).DeepCopy() @@ -288,6 +254,16 @@ func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { in, out := &in.CompletionTime, &out.CompletionTime *out = (*in).DeepCopy() } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(int64) + **out = **in + } + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -297,90 +273,105 @@ func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunStatus. -func (in *AgentPlaybookRunStatus) DeepCopy() *AgentPlaybookRunStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunStatus. +func (in *AgentRunStatus) DeepCopy() *AgentRunStatus { if in == nil { return nil } - out := new(AgentPlaybookRunStatus) + out := new(AgentRunStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookSpec) DeepCopyInto(out *AgentPlaybookSpec) { +func (in *AgentSkillCardRef) DeepCopyInto(out *AgentSkillCardRef) { *out = *in - if in.Stages != nil { - in, out := &in.Stages, &out.Stages - *out = make([]AgentPlaybookStage, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookSpec. -func (in *AgentPlaybookSpec) DeepCopy() *AgentPlaybookSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCardRef. +func (in *AgentSkillCardRef) DeepCopy() *AgentSkillCardRef { if in == nil { return nil } - out := new(AgentPlaybookSpec) + out := new(AgentSkillCardRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookStage) DeepCopyInto(out *AgentPlaybookStage) { +func (in *AgentSkillCollectionRef) DeepCopyInto(out *AgentSkillCollectionRef) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookStage. -func (in *AgentPlaybookStage) DeepCopy() *AgentPlaybookStage { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCollectionRef. +func (in *AgentSkillCollectionRef) DeepCopy() *AgentSkillCollectionRef { if in == nil { return nil } - out := new(AgentPlaybookStage) + out := new(AgentSkillCollectionRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookStatus) DeepCopyInto(out *AgentPlaybookStatus) { +func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]AgentProviderRef, len(*in)) + copy(*out, *in) + } + if in.SkillCards != nil { + in, out := &in.SkillCards, &out.SkillCards + *out = make([]AgentSkillCardRef, len(*in)) + copy(*out, *in) + } + if in.SkillCollections != nil { + in, out := &in.SkillCollections, &out.SkillCollections + *out = make([]AgentSkillCollectionRef, len(*in)) + copy(*out, *in) + } + if in.Params != nil { + in, out := &in.Params, &out.Params + *out = make([]AgentParam, len(*in)) + copy(*out, *in) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookStatus. -func (in *AgentPlaybookStatus) DeepCopy() *AgentPlaybookStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. +func (in *AgentSpec) DeepCopy() *AgentSpec { if in == nil { return nil } - out := new(AgentPlaybookStatus) + out := new(AgentSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentProviderRef) DeepCopyInto(out *AgentProviderRef) { +func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentProviderRef. -func (in *AgentProviderRef) DeepCopy() *AgentProviderRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. +func (in *AgentStatus) DeepCopy() *AgentStatus { if in == nil { return nil } - out := new(AgentProviderRef) + out := new(AgentStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRun) DeepCopyInto(out *AgentRun) { +func (in *AgentWorkflow) DeepCopyInto(out *AgentWorkflow) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -388,18 +379,18 @@ func (in *AgentRun) DeepCopyInto(out *AgentRun) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRun. -func (in *AgentRun) DeepCopy() *AgentRun { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflow. +func (in *AgentWorkflow) DeepCopy() *AgentWorkflow { if in == nil { return nil } - out := new(AgentRun) + out := new(AgentWorkflow) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentRun) DeepCopyObject() runtime.Object { +func (in *AgentWorkflow) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -407,31 +398,31 @@ func (in *AgentRun) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunList) DeepCopyInto(out *AgentRunList) { +func (in *AgentWorkflowList) DeepCopyInto(out *AgentWorkflowList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentRun, len(*in)) + *out = make([]AgentWorkflow, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunList. -func (in *AgentRunList) DeepCopy() *AgentRunList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowList. +func (in *AgentWorkflowList) DeepCopy() *AgentWorkflowList { if in == nil { return nil } - out := new(AgentRunList) + out := new(AgentWorkflowList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentRunList) DeepCopyObject() runtime.Object { +func (in *AgentWorkflowList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -439,37 +430,66 @@ func (in *AgentRunList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunModelSelection) DeepCopyInto(out *AgentRunModelSelection) { +func (in *AgentWorkflowRun) DeepCopyInto(out *AgentWorkflowRun) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunModelSelection. -func (in *AgentRunModelSelection) DeepCopy() *AgentRunModelSelection { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRun. +func (in *AgentWorkflowRun) DeepCopy() *AgentWorkflowRun { if in == nil { return nil } - out := new(AgentRunModelSelection) + out := new(AgentWorkflowRun) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentWorkflowRun) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunParam) DeepCopyInto(out *AgentRunParam) { +func (in *AgentWorkflowRunList) DeepCopyInto(out *AgentWorkflowRunList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AgentWorkflowRun, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunParam. -func (in *AgentRunParam) DeepCopy() *AgentRunParam { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunList. +func (in *AgentWorkflowRunList) DeepCopy() *AgentWorkflowRunList { if in == nil { return nil } - out := new(AgentRunParam) + out := new(AgentWorkflowRunList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentWorkflowRunList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { +func (in *AgentWorkflowRunSpec) DeepCopyInto(out *AgentWorkflowRunSpec) { *out = *in if in.Models != nil { in, out := &in.Models, &out.Models @@ -497,19 +517,39 @@ func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunSpec. -func (in *AgentRunSpec) DeepCopy() *AgentRunSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunSpec. +func (in *AgentWorkflowRunSpec) DeepCopy() *AgentWorkflowRunSpec { if in == nil { return nil } - out := new(AgentRunSpec) + out := new(AgentWorkflowRunSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { +func (in *AgentWorkflowRunStageStatus) DeepCopyInto(out *AgentWorkflowRunStageStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunStageStatus. +func (in *AgentWorkflowRunStageStatus) DeepCopy() *AgentWorkflowRunStageStatus { + if in == nil { + return nil + } + out := new(AgentWorkflowRunStageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentWorkflowRunStatus) DeepCopyInto(out *AgentWorkflowRunStatus) { *out = *in + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make([]AgentWorkflowRunStageStatus, len(*in)) + copy(*out, *in) + } if in.StartTime != nil { in, out := &in.StartTime, &out.StartTime *out = (*in).DeepCopy() @@ -518,16 +558,6 @@ func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { in, out := &in.CompletionTime, &out.CompletionTime *out = (*in).DeepCopy() } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(int64) - **out = **in - } - if in.SecretKeyRef != nil { - in, out := &in.SecretKeyRef, &out.SecretKeyRef - *out = new(corev1.LocalObjectReference) - **out = **in - } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -537,83 +567,53 @@ func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunStatus. -func (in *AgentRunStatus) DeepCopy() *AgentRunStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunStatus. +func (in *AgentWorkflowRunStatus) DeepCopy() *AgentWorkflowRunStatus { if in == nil { return nil } - out := new(AgentRunStatus) + out := new(AgentWorkflowRunStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSkillCardRef) DeepCopyInto(out *AgentSkillCardRef) { +func (in *AgentWorkflowSpec) DeepCopyInto(out *AgentWorkflowSpec) { *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCardRef. -func (in *AgentSkillCardRef) DeepCopy() *AgentSkillCardRef { - if in == nil { - return nil + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make([]AgentWorkflowStage, len(*in)) + copy(*out, *in) } - out := new(AgentSkillCardRef) - in.DeepCopyInto(out) - return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSkillCollectionRef) DeepCopyInto(out *AgentSkillCollectionRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCollectionRef. -func (in *AgentSkillCollectionRef) DeepCopy() *AgentSkillCollectionRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowSpec. +func (in *AgentWorkflowSpec) DeepCopy() *AgentWorkflowSpec { if in == nil { return nil } - out := new(AgentSkillCollectionRef) + out := new(AgentWorkflowSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { +func (in *AgentWorkflowStage) DeepCopyInto(out *AgentWorkflowStage) { *out = *in - if in.Providers != nil { - in, out := &in.Providers, &out.Providers - *out = make([]AgentProviderRef, len(*in)) - copy(*out, *in) - } - if in.SkillCards != nil { - in, out := &in.SkillCards, &out.SkillCards - *out = make([]AgentSkillCardRef, len(*in)) - copy(*out, *in) - } - if in.SkillCollections != nil { - in, out := &in.SkillCollections, &out.SkillCollections - *out = make([]AgentSkillCollectionRef, len(*in)) - copy(*out, *in) - } - if in.Params != nil { - in, out := &in.Params, &out.Params - *out = make([]AgentParam, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. -func (in *AgentSpec) DeepCopy() *AgentSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowStage. +func (in *AgentWorkflowStage) DeepCopy() *AgentWorkflowStage { if in == nil { return nil } - out := new(AgentSpec) + out := new(AgentWorkflowStage) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { +func (in *AgentWorkflowStatus) DeepCopyInto(out *AgentWorkflowStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -624,12 +624,12 @@ func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. -func (in *AgentStatus) DeepCopy() *AgentStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowStatus. +func (in *AgentWorkflowStatus) DeepCopy() *AgentWorkflowStatus { if in == nil { return nil } - out := new(AgentStatus) + out := new(AgentWorkflowStatus) in.DeepCopyInto(out) return out } diff --git a/changes/unreleased/agentplaybook-controllers.yaml b/changes/unreleased/agentplaybook-controllers.yaml deleted file mode 100644 index d70f709a..00000000 --- a/changes/unreleased/agentplaybook-controllers.yaml +++ /dev/null @@ -1,10 +0,0 @@ -kind: feature -description: > - Add AgentPlaybook and AgentPlaybookRun controllers for sequential - multi-stage agent orchestration. AgentPlaybook validates referenced - Agents are Ready. AgentPlaybookRun creates AgentRuns per stage - sequentially, forwarding params, models, env, and envFrom from - the playbook run to each stage. Includes deterministic AgentRun - naming, playbook guide passthrough via KONVEYOR_PLAYBOOK_INSTRUCTIONS, - and a fix for the Sandbox finished-reason constant (Succeeded → - PodSucceeded) to match Agent Sandbox v0.5.0. diff --git a/changes/unreleased/agentworkflow-controllers.yaml b/changes/unreleased/agentworkflow-controllers.yaml new file mode 100644 index 00000000..829cdba4 --- /dev/null +++ b/changes/unreleased/agentworkflow-controllers.yaml @@ -0,0 +1,10 @@ +kind: feature +description: > + Add AgentWorkflow and AgentWorkflowRun controllers for sequential + multi-stage agent orchestration. AgentWorkflow validates referenced + Agents are Ready. AgentWorkflowRun creates AgentRuns per stage + sequentially, forwarding params, models, env, and envFrom from + the workflow run to each stage. Includes deterministic AgentRun + naming, workflow guide passthrough via KONVEYOR_WORKFLOW_GUIDE, + and a fix for the Sandbox finished-reason constant (Succeeded → + PodSucceeded) to match Agent Sandbox v0.5.0. diff --git a/changes/unreleased/scaffold-crds.yaml b/changes/unreleased/scaffold-crds.yaml index cd0942f2..bd98b2da 100644 --- a/changes/unreleased/scaffold-crds.yaml +++ b/changes/unreleased/scaffold-crds.yaml @@ -1,5 +1,5 @@ kind: feature description: > - Defined Agent, AgentRun, AgentPlaybook, AgentPlaybookRun, SkillCard, + Defined Agent, AgentRun, AgentWorkflow, AgentWorkflowRun, SkillCard, SkillCollection, and LLMProvider CRDs with CEL validation, immutability rules, and printer columns. diff --git a/cmd/main.go b/cmd/main.go index 3c60e8d0..2c1cd495 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -215,18 +215,19 @@ func main() { setupLog.Error(err, "Failed to create controller", "controller", "AgentRun") os.Exit(1) } - if err := (&controller.AgentPlaybookReconciler{ + if err := (&controller.AgentWorkflowReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "Failed to create controller", "controller", "AgentPlaybook") + setupLog.Error(err, "Failed to create controller", "controller", "AgentWorkflow") os.Exit(1) } - if err := (&controller.AgentPlaybookRunReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + if err := (&controller.AgentWorkflowRunReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("agentworkflowrun-controller"), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "Failed to create controller", "controller", "AgentPlaybookRun") + setupLog.Error(err, "Failed to create controller", "controller", "AgentWorkflowRun") os.Exit(1) } // +kubebuilder:scaffold:builder diff --git a/config/crd/bases/konveyor.io_agentplaybookruns.yaml b/config/crd/bases/konveyor.io_agentworkflowruns.yaml similarity index 95% rename from config/crd/bases/konveyor.io_agentplaybookruns.yaml rename to config/crd/bases/konveyor.io_agentworkflowruns.yaml index 88ab7527..43f80b85 100644 --- a/config/crd/bases/konveyor.io_agentplaybookruns.yaml +++ b/config/crd/bases/konveyor.io_agentworkflowruns.yaml @@ -4,21 +4,21 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.21.0 - name: agentplaybookruns.konveyor.io + name: agentworkflowruns.konveyor.io spec: group: konveyor.io names: - kind: AgentPlaybookRun - listKind: AgentPlaybookRunList - plural: agentplaybookruns + kind: AgentWorkflowRun + listKind: AgentWorkflowRunList + plural: agentworkflowruns shortNames: - - apr - singular: agentplaybookrun + - awr + singular: agentworkflowrun scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.playbookRef - name: Playbook + - jsonPath: .spec.workflowRef + name: Workflow type: string - jsonPath: .status.phase name: Phase @@ -33,8 +33,8 @@ spec: schema: openAPIV3Schema: description: |- - AgentPlaybookRun is a request to execute an AgentPlaybook. It references - an AgentPlaybook and carries generic parameters. The controller orchestrates + AgentWorkflowRun is a request to execute an AgentWorkflow. It references + an AgentWorkflow and carries generic parameters. The controller orchestrates execution: creates an AgentRun per stage, manages cross-stage handoff via committed files on a shared target branch. properties: @@ -57,7 +57,7 @@ spec: type: object spec: description: |- - AgentPlaybookRunSpec defines the desired state of an AgentPlaybookRun. + AgentWorkflowRunSpec defines the desired state of an AgentWorkflowRun. The spec is immutable once created — delete and recreate to change values. properties: env: @@ -328,27 +328,27 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - playbookRef: - description: PlaybookRef is the name of the AgentPlaybook CR to execute. + workflowRef: + description: WorkflowRef is the name of the AgentWorkflow CR to execute. minLength: 1 type: string required: - - playbookRef + - workflowRef type: object x-kubernetes-validations: - message: spec is immutable rule: self == oldSelf status: - description: AgentPlaybookRunStatus defines the observed state of an AgentPlaybookRun. + description: AgentWorkflowRunStatus defines the observed state of an AgentWorkflowRun. properties: completionTime: - description: CompletionTime is the time the playbook run finished. + description: CompletionTime is the time the workflow run finished. format: date-time type: string conditions: description: |- Conditions represent the latest available observations of the - AgentPlaybookRun's state. + AgentWorkflowRun's state. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -417,7 +417,7 @@ spec: type: integer phase: default: Pending - description: Phase is the current phase of the overall playbook run. + description: Phase is the current phase of the overall workflow run. enum: - Pending - Running @@ -428,8 +428,8 @@ spec: description: Stages tracks the status of each stage. items: description: |- - AgentPlaybookRunStageStatus tracks the status of a single stage within - a playbook run. + AgentWorkflowRunStageStatus tracks the status of a single stage within + a workflow run. properties: agentRunName: description: AgentRunName is the name of the AgentRun CR created @@ -437,7 +437,7 @@ spec: type: string name: description: Name is the stage name, matching a stage in the - AgentPlaybook. + AgentWorkflow. type: string phase: description: Phase is the current phase of this stage. @@ -456,7 +456,7 @@ spec: - name x-kubernetes-list-type: map startTime: - description: StartTime is the time the playbook run started. + description: StartTime is the time the workflow run started. format: date-time type: string type: object diff --git a/config/crd/bases/konveyor.io_agentplaybooks.yaml b/config/crd/bases/konveyor.io_agentworkflows.yaml similarity index 92% rename from config/crd/bases/konveyor.io_agentplaybooks.yaml rename to config/crd/bases/konveyor.io_agentworkflows.yaml index 3a2295bc..fa625db2 100644 --- a/config/crd/bases/konveyor.io_agentplaybooks.yaml +++ b/config/crd/bases/konveyor.io_agentworkflows.yaml @@ -4,16 +4,16 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.21.0 - name: agentplaybooks.konveyor.io + name: agentworkflows.konveyor.io spec: group: konveyor.io names: - kind: AgentPlaybook - listKind: AgentPlaybookList - plural: agentplaybooks + kind: AgentWorkflow + listKind: AgentWorkflowList + plural: agentworkflows shortNames: - - ap - singular: agentplaybook + - aw + singular: agentworkflow scope: Namespaced versions: - additionalPrinterColumns: @@ -27,9 +27,9 @@ spec: schema: openAPIV3Schema: description: |- - AgentPlaybook is a reusable playbook combining a high-level guide with an + AgentWorkflow is a reusable workflow combining a high-level guide with an ordered sequence of stages. Each stage references an Agent and carries - instructions. An AgentPlaybook is a template — creating one does not + instructions. An AgentWorkflow is a template — creating one does not execute anything. properties: apiVersion: @@ -50,7 +50,7 @@ spec: metadata: type: object spec: - description: AgentPlaybookSpec defines the desired state of an AgentPlaybook. + description: AgentWorkflowSpec defines the desired state of an AgentWorkflow. properties: guide: description: |- @@ -66,7 +66,7 @@ spec: handoff files. items: description: |- - AgentPlaybookStage defines one stage in a playbook. + AgentWorkflowStage defines one stage in a workflow. Each stage references an Agent and carries instructions. properties: agentRef: @@ -81,7 +81,7 @@ spec: type: string name: description: |- - Name is the stage name, unique within the playbook. + Name is the stage name, unique within the workflow. Must be a valid Kubernetes label value (lowercase alphanumeric, hyphens, dots, max 63 chars) since it is used in labels on child AgentRun resources. @@ -102,12 +102,12 @@ spec: - stages type: object status: - description: AgentPlaybookStatus defines the observed state of an AgentPlaybook. + description: AgentWorkflowStatus defines the observed state of an AgentWorkflow. properties: conditions: description: |- Conditions represent the latest available observations of the - AgentPlaybook's state. + AgentWorkflow's state. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5975c64e..eb601fbc 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,6 @@ resources: - bases/konveyor.io_llmproviders.yaml - bases/konveyor.io_agents.yaml - bases/konveyor.io_agentruns.yaml -- bases/konveyor.io_agentplaybooks.yaml -- bases/konveyor.io_agentplaybookruns.yaml +- bases/konveyor.io_agentworkflows.yaml +- bases/konveyor.io_agentworkflowruns.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3d4cc4eb..6046415c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,13 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - "" resources: @@ -38,10 +45,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns - - agentplaybooks - agentruns - agents + - agentworkflowruns + - agentworkflows - llmproviders - skillcards - skillcollections @@ -56,10 +63,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns/finalizers - - agentplaybooks/finalizers - agentruns/finalizers - agents/finalizers + - agentworkflowruns/finalizers + - agentworkflows/finalizers - llmproviders/finalizers - skillcards/finalizers - skillcollections/finalizers @@ -68,10 +75,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns/status - - agentplaybooks/status - agentruns/status - agents/status + - agentworkflowruns/status + - agentworkflows/status - llmproviders/status - skillcards/status - skillcollections/status diff --git a/docs/adr/0001-agentic-platform-crd-architecture.md b/docs/adr/0001-agentic-platform-crd-architecture.md index ba585e0e..c1828702 100644 --- a/docs/adr/0001-agentic-platform-crd-architecture.md +++ b/docs/adr/0001-agentic-platform-crd-architecture.md @@ -48,14 +48,14 @@ Kubernetes API with the emerging OCI skills ecosystem. | **SkillCollection** | Group of skills. Each entry references a skill by OCI image ref, git source, or SkillCard CR name. | | **LLMProvider** | LLM service endpoint, credentials (Secret ref), and available models with context window sizes and optional tier labels. | | **Agent** | Template declaring what is available for execution. References one or more LLMProviders, SkillCards, SkillCollections, a container image, a prompt, and declares typed parameters (inputs the AgentRun must supply). Does not select a specific model — model selection happens at execution time. Analogous to a Tekton Task. | -| **AgentPlaybook** | Ordered sequence of stages. Each stage references an Agent and carries instructions. Stages execute sequentially with fresh agent sessions. Cross-stage continuity is through git branch content. | +| **AgentWorkflow** | Ordered sequence of stages. Each stage references an Agent and carries instructions. Stages execute sequentially with fresh agent sessions. Cross-stage continuity is through git branch content. | **Execution resources** (created to trigger work): | CRD | Purpose | |-----|---------| | **AgentRun** | Execute a single Agent with specific values. References an Agent, selects models, supplies parameter values, carries instructions, and may include additional `env` and `envFrom` entries passed through to the Sandbox. The controller validates params against the Agent's declarations, creates a Sandbox, and tracks status. Analogous to a Tekton TaskRun. | -| **AgentPlaybookRun** | Execute an AgentPlaybook. Creates AgentRun CRs sequentially per stage, all sharing the same target branch. Each stage reads the previous stage's committed handoff files. | +| **AgentWorkflowRun** | Execute an AgentWorkflow. Creates AgentRun CRs sequentially per stage, all sharing the same target branch. Each stage reads the previous stage's committed handoff files. | ### Naming alignment with skillimage @@ -211,7 +211,7 @@ isolation requires OpenShell (NVIDIA) filesystem policy enforcement, which can deny reads to the Secret mount path at the kernel level. The harness manages all credentialed git operations. -No PVCs survive between runs. Cross-stage continuity in playbooks +No PVCs survive between runs. Cross-stage continuity in workflows is through committed files on the shared branch. Parallel agents on the same application use different branches. @@ -228,10 +228,10 @@ repo on a `konveyor/` branch. This follows the pattern established by the assets-generation enhancement and implemented in tackle2-addon-platform. -### AgentPlaybook execution model +### AgentWorkflow execution model -An AgentPlaybook is a flat sequence of stages. Each stage references -an Agent and carries instructions. The AgentPlaybookRun controller +An AgentWorkflow is a flat sequence of stages. Each stage references +an Agent and carries instructions. The AgentWorkflowRun controller creates AgentRuns sequentially, all targeting the same branch. Each stage gets a fresh agent session. Cross-stage knowledge @@ -240,7 +240,7 @@ transfer happens through committed files on the branch: 1. **Handoff files** — each stage commits `.konveyor/handoff.md` summarizing what was accomplished and what remains. The next stage's agent reads this on checkout. -2. **Playbook guide** — the playbook's guide is committed as +2. **Workflow guide** — the workflow's guide is committed as `.konveyor/guide.md` before stage 1, providing ambient context. Prompt composition at execution time: @@ -269,7 +269,7 @@ executions of that Agent, enabling organizational learning. A POC exists in dymurray/tackle2-addon-kai using mempalace. Memory service integration is deferred to Phase 4 — after the core execution -model (AgentRun), playbooks (AgentPlaybook), and configurable git +model (AgentRun), workflows (AgentWorkflow), and configurable git strategy are proven. ### Hub as a data service @@ -450,16 +450,16 @@ otherwise need to build ourselves. ### Tekton as the orchestration layer Tekton Tasks/Pipelines were considered for orchestrating multi-stage -agent playbooks. Deferred, not rejected: the MVP requires only +agent workflows. Deferred, not rejected: the MVP requires only sequential stage execution, and a custom controller is simpler than adding a Tekton dependency. The architecture does not preclude Tekton -integration later — AgentPlaybookRun could generate Tekton +integration later — AgentWorkflowRun could generate Tekton PipelineRuns as an implementation detail when users need conditionals, parallelism, retries, or supply chain signing. ### Stages with phases and session continuity -We originally designed AgentPlaybook with stages containing multiple +We originally designed AgentWorkflow with stages containing multiple phases, where phases within a stage shared session state via a PVC (SQLite database). This provided full conversation continuity within a stage. diff --git a/docs/adr/0003-hub-curated-api-for-agent-resources.md b/docs/adr/0003-hub-curated-api-for-agent-resources.md index 720866e2..db2bae72 100644 --- a/docs/adr/0003-hub-curated-api-for-agent-resources.md +++ b/docs/adr/0003-hub-curated-api-for-agent-resources.md @@ -8,7 +8,7 @@ The agentic platform controller introduces seven CRDs under the `konveyor.io` API group (SkillCard, SkillCollection, LLMProvider, -Agent, AgentRun, AgentPlaybook, AgentPlaybookRun). The UI needs to +Agent, AgentRun, AgentWorkflow, AgentWorkflowRun). The UI needs to create, read, update, and delete these resources. The question is how the UI accesses them. diff --git a/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md b/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md index b373efb8..270af814 100644 --- a/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md +++ b/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md @@ -12,7 +12,7 @@ Each stage was a Go package that constructed multi-turn ACP prompts from YAML recipe files and an embedded skill bundle. The harness owned both stage sequencing and migration intelligence. -With the AgentPlaybookRun controller (ADR 0001) handling stage +With the AgentWorkflowRun controller (ADR 0001) handling stage sequencing, the harness no longer needs orchestration logic. Meanwhile, the SkillCard CRD and OCI-based skill packaging provide a clean mechanism for delivering migration knowledge to agent pods at runtime. @@ -45,7 +45,7 @@ patterns to apply. Its responsibilities are: 8. Discover skills from `/opt/skills/*/SKILL.md` (glob) 9. Build a single prompt from four context layers: - `KONVEYOR_PROMPT` — agent-level standing instructions - - `KONVEYOR_PLAYBOOK_INSTRUCTIONS` — playbook guide context + - `KONVEYOR_WORKFLOW_GUIDE` — workflow guide context - Skill content (concatenated from all discovered skills) - `KONVEYOR_INSTRUCTIONS` — stage-specific task 10. Start a filesystem watcher for incremental push @@ -289,7 +289,7 @@ reconciler that watches pod exit codes. - **Test harness scaffolding.** `hack/harness-test/setup.sh` builds skill OCI images locally, loads them into Kind, and applies - SkillCard + Agent + AgentPlaybook + AgentPlaybookRun CRs for + SkillCard + Agent + AgentWorkflow + AgentWorkflowRun CRs for end-to-end testing. diff --git a/docs/slides/demo-2026-07-16.md b/docs/slides/demo-2026-07-16.md index 905e146e..8af5ed4a 100644 --- a/docs/slides/demo-2026-07-16.md +++ b/docs/slides/demo-2026-07-16.md @@ -51,7 +51,7 @@ A **Kubernetes controller** for managing AI agent workloads in Konveyor | **SkillCollection** | Group of related skills | | **LLMProvider** | LLM endpoint, credentials, model catalog | | **Agent** | Template: image, providers, skills, prompt, parameters | -| **AgentPlaybook** | Ordered sequence of stages, each referencing an Agent | +| **AgentWorkflow** | Ordered sequence of stages, each referencing an Agent | --- @@ -62,7 +62,7 @@ A **Kubernetes controller** for managing AI agent workloads in Konveyor | CRD | Purpose | |---|---| | **AgentRun** | Concrete invocation of an Agent with specific values | -| **AgentPlaybookRun** | Runs a playbook — creates sequential AgentRuns | +| **AgentWorkflowRun** | Runs a workflow — creates sequential AgentRuns | **Agent vs AgentRun** — the key distinction: - **Agent** = what's available (template) @@ -87,8 +87,8 @@ Parameters are **opaque** — the controller injects them but never interprets t Hub exposes **purpose-built REST endpoints** for all agent resources: ``` -/hub/agents /hub/agentplaybooks -/hub/agentruns /hub/agentplaybookruns +/hub/agents /hub/agentworkflows +/hub/agentruns /hub/agentworkflowruns /hub/skillcards /hub/skillcollections /hub/llmproviders ``` @@ -150,9 +150,9 @@ Artifacts: session state, handoff markdown, verification reports, execution logs --- -# Multi-Stage Playbooks +# Multi-Stage Workflows -**AgentPlaybook** defines ordered stages, each with its own Agent: +**AgentWorkflow** defines ordered stages, each with its own Agent: | Stage | Agent | What it does | |---|---|---| @@ -160,7 +160,7 @@ Artifacts: session state, handoff markdown, verification reports, execution logs | **migrate** | code-migrator | Execute the migration plan | | **verify** | test-runner | Run tests and verify correctness | -Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a shared branch. The playbook guide is injected as `KONVEYOR_PLAYBOOK_INSTRUCTIONS`. +Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a shared branch. The workflow guide is injected as `KONVEYOR_WORKFLOW_GUIDE`. --- @@ -182,7 +182,7 @@ Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a sh # What's Next -**Landing now:** PRs #33 (harness), #35 (client stack), #36 (playbook controllers) +**Landing now:** PRs #33 (harness), #35 (client stack), #36 (workflow controllers) **Roadmap:** diff --git a/hack/harness-test/resources.yaml b/hack/harness-test/resources.yaml index 2fd3dbb7..12cf9818 100644 --- a/hack/harness-test/resources.yaml +++ b/hack/harness-test/resources.yaml @@ -1,5 +1,5 @@ # Shared resources for harness integration tests in Kind. -# Creates: LLMProvider (used by playbook-resources.yaml) +# Creates: LLMProvider (used by workflow-resources.yaml) # # Usage: # hack/harness-test/setup.sh diff --git a/hack/harness-test/playbook-resources.yaml b/hack/harness-test/workflow-resources.yaml similarity index 94% rename from hack/harness-test/playbook-resources.yaml rename to hack/harness-test/workflow-resources.yaml index 08594126..8824681a 100644 --- a/hack/harness-test/playbook-resources.yaml +++ b/hack/harness-test/workflow-resources.yaml @@ -1,4 +1,4 @@ -# Example resources for testing the AgentPlaybook + AgentPlaybookRun flow. +# Example resources for testing the AgentWorkflow + AgentWorkflowRun flow. # Migrates the coolstore Java EE app to Quarkus using 3 stages. # # Prerequisites: @@ -7,7 +7,7 @@ # # Usage: # kubectl apply -f hack/harness-test/resources.yaml -# kubectl apply -f hack/harness-test/playbook-resources.yaml +# kubectl apply -f hack/harness-test/workflow-resources.yaml --- apiVersion: konveyor.io/v1alpha1 @@ -130,7 +130,7 @@ spec: --- apiVersion: konveyor.io/v1alpha1 -kind: AgentPlaybook +kind: AgentWorkflow metadata: name: java-ee-to-quarkus spec: @@ -148,11 +148,11 @@ spec: --- apiVersion: konveyor.io/v1alpha1 -kind: AgentPlaybookRun +kind: AgentWorkflowRun metadata: name: coolstore-migration-__TIMESTAMP__ spec: - playbookRef: java-ee-to-quarkus + workflowRef: java-ee-to-quarkus models: - role: primary provider: gcp-vertex-ai diff --git a/harness/README.md b/harness/README.md index f656ef80..0c3dcc4e 100644 --- a/harness/README.md +++ b/harness/README.md @@ -23,7 +23,7 @@ └──────────────────────────────────────────────────────┘ ``` -The harness sends **one prompt** per stage. The AgentPlaybookRun controller handles stage sequencing — the harness is identical in every stage image. +The harness sends **one prompt** per stage. The AgentWorkflowRun controller handles stage sequencing — the harness is identical in every stage image. --- @@ -70,7 +70,7 @@ All configuration is via environment variables — there is no config file or `i | `HARNESS_WORK_DIR` | `/workspace/repo` | Clone directory | | `HARNESS_SKILLS_DIR` | `/opt/skills` | Skills mount directory | | `KONVEYOR_PROMPT` | — | Agent-level standing instructions | -| `KONVEYOR_PLAYBOOK_INSTRUCTIONS` | — | Playbook guide context | +| `KONVEYOR_WORKFLOW_GUIDE` | — | Workflow guide context | | `KONVEYOR_INSTRUCTIONS` | — | Stage-specific task instructions | --- diff --git a/harness/internal/config/config.go b/harness/internal/config/config.go index 4d4a9c5d..ac9dc1c4 100644 --- a/harness/internal/config/config.go +++ b/harness/internal/config/config.go @@ -71,9 +71,9 @@ func LoadFromEnv() (*Config, error) { // workflowGuideFromEnv reads the workflow guide the controller injects. // -// konveyor/agentic-controller#80 renames KONVEYOR_PLAYBOOK_INSTRUCTIONS to -// KONVEYOR_WORKFLOW_GUIDE. Reading both means the harness works either side of -// that merge; drop the fallback once #80 has landed everywhere. +// The canonical env var is KONVEYOR_WORKFLOW_GUIDE (set by the controller). +// KONVEYOR_PLAYBOOK_INSTRUCTIONS is the legacy name; drop the fallback +// once all deployed controllers use the new name. func workflowGuideFromEnv() string { if v := os.Getenv("KONVEYOR_WORKFLOW_GUIDE"); v != "" { return v diff --git a/harness/internal/config/config_test.go b/harness/internal/config/config_test.go index 72bd3b91..34e78e45 100644 --- a/harness/internal/config/config_test.go +++ b/harness/internal/config/config_test.go @@ -158,8 +158,8 @@ func TestLoadFromEnvReadsPromptLayers(t *testing.T) { } } -// #80 renames the env var; the harness reads either so merge order does not -// matter. Remove with the fallback once #80 has landed everywhere. +// The legacy env var KONVEYOR_PLAYBOOK_INSTRUCTIONS is still read as a +// fallback. Remove this test when the fallback is dropped. func TestLoadFromEnvFallsBackToPlaybookInstructions(t *testing.T) { clearKonveyorEnv(t) setRequiredEnv(t) diff --git a/internal/controller/agentrun_controller.go b/internal/controller/agentrun_controller.go index c9139340..9c884e7c 100644 --- a/internal/controller/agentrun_controller.go +++ b/internal/controller/agentrun_controller.go @@ -368,7 +368,13 @@ func (r *AgentRunReconciler) createSandbox( }, }, Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, + // Never restart — a failed container must reach a terminal + // phase so the AgentRun (and workflow stage) can observe + // the failure. OnFailure would cause infinite crashloops + // (#51). The tradeoff is that transient failures (image + // pull blips, node eviction) are not retried. Bounded + // retry (backoffLimit-style) can be added later if needed. + RestartPolicy: corev1.RestartPolicyNever, Containers: []corev1.Container{ { Name: "agent", diff --git a/internal/controller/agentrun_controller_test.go b/internal/controller/agentrun_controller_test.go index 5b682de6..f5070fa6 100644 --- a/internal/controller/agentrun_controller_test.go +++ b/internal/controller/agentrun_controller_test.go @@ -352,6 +352,9 @@ var _ = Describe("AgentRun Controller", func() { Expect(sandbox.Spec.PodTemplate.ObjectMeta.Labels).To(HaveKeyWithValue("konveyor.io/agentrun", name)) Expect(sandbox.Spec.PodTemplate.ObjectMeta.Labels).To(HaveKeyWithValue("konveyor.io/agent", agentName)) + By("verifying restartPolicy is Never so failed stages are observable (#51)") + Expect(sandbox.Spec.PodTemplate.Spec.RestartPolicy).To(Equal(corev1.RestartPolicyNever)) + By("verifying the single-key provider credential is injected as API_KEY") container := sandbox.Spec.PodTemplate.Spec.Containers[0] var apiKey *corev1.EnvVar diff --git a/internal/controller/agentplaybook_controller.go b/internal/controller/agentworkflow_controller.go similarity index 60% rename from internal/controller/agentplaybook_controller.go rename to internal/controller/agentworkflow_controller.go index fc9bc29c..c20260ce 100644 --- a/internal/controller/agentplaybook_controller.go +++ b/internal/controller/agentworkflow_controller.go @@ -36,44 +36,44 @@ import ( ) const ( - // playbookAgentRefIndexField is the field index for looking up - // AgentPlaybooks by their stages' agentRef values. - playbookAgentRefIndexField = ".spec.stages.agentRef" + // workflowAgentRefIndexField is the field index for looking up + // AgentWorkflows by their stages' agentRef values. + workflowAgentRefIndexField = ".spec.stages.agentRef" ) -// AgentPlaybookReconciler reconciles an AgentPlaybook object. -type AgentPlaybookReconciler struct { +// AgentWorkflowReconciler reconciles an AgentWorkflow object. +type AgentWorkflowReconciler struct { client.Client Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks/finalizers,verbs=update +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows/finalizers,verbs=update -// Reconcile handles AgentPlaybook reconciliation. +// Reconcile handles AgentWorkflow reconciliation. // // The controller validates that all Agents referenced by stages exist and -// are Ready, then reports aggregate readiness on the AgentPlaybook. -func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// are Ready, then reports aggregate readiness on the AgentWorkflow. +func (r *AgentWorkflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - var playbook konveyoriov1alpha1.AgentPlaybook - if err := r.Get(ctx, req.NamespacedName, &playbook); err != nil { + var workflow konveyoriov1alpha1.AgentWorkflow + if err := r.Get(ctx, req.NamespacedName, &workflow); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - logger.V(1).Info("Reconciling AgentPlaybook", "name", playbook.Name) + logger.V(1).Info("Reconciling AgentWorkflow", "name", workflow.Name) - original := playbook.DeepCopy() - playbook.Status.ObservedGeneration = playbook.Generation + original := workflow.DeepCopy() + workflow.Status.ObservedGeneration = workflow.Generation var notReadyReasons []string // Validate that each stage's Agent exists and is Ready. - for _, stage := range playbook.Spec.Stages { + for _, stage := range workflow.Spec.Stages { var agent konveyoriov1alpha1.Agent - agentKey := types.NamespacedName{Namespace: playbook.Namespace, Name: stage.AgentRef} + agentKey := types.NamespacedName{Namespace: workflow.Namespace, Name: stage.AgentRef} if err := r.Get(ctx, agentKey, &agent); err != nil { if errors.IsNotFound(err) { notReadyReasons = append(notReadyReasons, @@ -91,25 +91,25 @@ func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Reques } if len(notReadyReasons) == 0 { - meta.SetStatusCondition(&playbook.Status.Conditions, metav1.Condition{ + meta.SetStatusCondition(&workflow.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionTrue, - ObservedGeneration: playbook.Generation, + ObservedGeneration: workflow.Generation, Reason: "AllAgentsReady", Message: "All stage Agents are ready", }) } else { - meta.SetStatusCondition(&playbook.Status.Conditions, metav1.Condition{ + meta.SetStatusCondition(&workflow.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionFalse, - ObservedGeneration: playbook.Generation, + ObservedGeneration: workflow.Generation, Reason: "AgentsNotReady", Message: strings.Join(notReadyReasons, "; "), }) } - if err := r.Status().Patch(ctx, &playbook, client.MergeFrom(original)); err != nil { - logger.Error(err, "Failed to patch AgentPlaybook status") + if err := r.Status().Patch(ctx, &workflow, client.MergeFrom(original)); err != nil { + logger.Error(err, "Failed to patch AgentWorkflow status") return ctrl.Result{}, err } @@ -117,38 +117,38 @@ func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // SetupWithManager sets up the controller with the Manager. -func (r *AgentPlaybookReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Index AgentPlaybooks by their stages' agentRef values for +func (r *AgentWorkflowReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Index AgentWorkflows by their stages' agentRef values for // efficient reverse lookup when an Agent changes. if err := mgr.GetFieldIndexer().IndexField( context.Background(), - &konveyoriov1alpha1.AgentPlaybook{}, - playbookAgentRefIndexField, + &konveyoriov1alpha1.AgentWorkflow{}, + workflowAgentRefIndexField, func(obj client.Object) []string { - playbook := obj.(*konveyoriov1alpha1.AgentPlaybook) - refs := make([]string, len(playbook.Spec.Stages)) - for i, stage := range playbook.Spec.Stages { + workflow := obj.(*konveyoriov1alpha1.AgentWorkflow) + refs := make([]string, len(workflow.Spec.Stages)) + for i, stage := range workflow.Spec.Stages { refs[i] = stage.AgentRef } return refs }, ); err != nil { - return fmt.Errorf("indexing %s: %w", playbookAgentRefIndexField, err) + return fmt.Errorf("indexing %s: %w", workflowAgentRefIndexField, err) } return ctrl.NewControllerManagedBy(mgr). - For(&konveyoriov1alpha1.AgentPlaybook{}). + For(&konveyoriov1alpha1.AgentWorkflow{}). Watches( &konveyoriov1alpha1.Agent{}, - handler.EnqueueRequestsFromMapFunc(r.findPlaybooksForAgent), + handler.EnqueueRequestsFromMapFunc(r.findWorkflowsForAgent), ). - Named("agentplaybook"). + Named("agentworkflow"). Complete(r) } -// findPlaybooksForAgent returns reconcile requests for all AgentPlaybooks +// findWorkflowsForAgent returns reconcile requests for all AgentWorkflows // that reference the given Agent in any stage. -func (r *AgentPlaybookReconciler) findPlaybooksForAgent( +func (r *AgentWorkflowReconciler) findWorkflowsForAgent( ctx context.Context, obj client.Object, ) []reconcile.Request { @@ -157,18 +157,18 @@ func (r *AgentPlaybookReconciler) findPlaybooksForAgent( return nil } - var playbookList konveyoriov1alpha1.AgentPlaybookList - if err := r.List(ctx, &playbookList, + var workflowList konveyoriov1alpha1.AgentWorkflowList + if err := r.List(ctx, &workflowList, client.InNamespace(agent.Namespace), - client.MatchingFields{playbookAgentRefIndexField: agent.Name}, + client.MatchingFields{workflowAgentRefIndexField: agent.Name}, ); err != nil { - log.FromContext(ctx).Error(err, "Failed to list AgentPlaybooks for Agent", + log.FromContext(ctx).Error(err, "Failed to list AgentWorkflows for Agent", "agent", agent.Name) return nil } - requests := make([]reconcile.Request, len(playbookList.Items)) - for i, pb := range playbookList.Items { + requests := make([]reconcile.Request, len(workflowList.Items)) + for i, pb := range workflowList.Items { requests[i] = reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: pb.Namespace, diff --git a/internal/controller/agentplaybook_controller_test.go b/internal/controller/agentworkflow_controller_test.go similarity index 76% rename from internal/controller/agentplaybook_controller_test.go rename to internal/controller/agentworkflow_controller_test.go index 95e2a8ff..65ca2990 100644 --- a/internal/controller/agentplaybook_controller_test.go +++ b/internal/controller/agentworkflow_controller_test.go @@ -29,7 +29,7 @@ import ( konveyoriov1alpha1 "github.com/konveyor/agentic-controller/api/v1alpha1" ) -var _ = Describe("AgentPlaybook Controller", func() { +var _ = Describe("AgentWorkflow Controller", func() { const ( timeout = 10 * time.Second interval = 250 * time.Millisecond @@ -37,23 +37,23 @@ var _ = Describe("AgentPlaybook Controller", func() { Context("when a stage references a non-existent Agent", func() { const ( - playbookName = "ap-ctrl-missing-agent" + workflowName = "ap-ctrl-missing-agent" ) It("should set Ready=False with AgentsNotReady", func() { - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "plan", AgentRef: "nonexistent-agent"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -62,13 +62,13 @@ var _ = Describe("AgentPlaybook Controller", func() { g.Expect(readyCond.Message).To(ContainSubstring("nonexistent-agent")) }, timeout, interval).Should(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) }) }) Context("when all stage Agents exist and are Ready", func() { const ( - playbookName = "ap-ctrl-all-ready" + workflowName = "ap-ctrl-all-ready" agentName1 = "ap-ctrl-agent-1" agentName2 = "ap-ctrl-agent-2" provName = "ap-prov-ready" @@ -99,21 +99,21 @@ var _ = Describe("AgentPlaybook Controller", func() { Expect(k8sClient.Create(ctx, agent2)).To(Succeed()) waitForAgentReady(agentName2) - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Guide: "Test migration playbook", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Guide: "Test migration workflow", + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "plan", AgentRef: agentName1, Instructions: "Create a plan"}, {Name: "execute", AgentRef: agentName2, Instructions: "Execute the plan"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -121,7 +121,7 @@ var _ = Describe("AgentPlaybook Controller", func() { g.Expect(readyCond.Reason).To(Equal("AllAgentsReady")) }, timeout, interval).Should(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent1)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent2)).To(Succeed()) }) diff --git a/internal/controller/agentplaybookrun_controller.go b/internal/controller/agentworkflowrun_controller.go similarity index 63% rename from internal/controller/agentplaybookrun_controller.go rename to internal/controller/agentworkflowrun_controller.go index b5ece91b..780f072b 100644 --- a/internal/controller/agentplaybookrun_controller.go +++ b/internal/controller/agentworkflowrun_controller.go @@ -20,12 +20,15 @@ import ( "context" "fmt" + "strings" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/events" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -36,40 +39,42 @@ import ( ) const ( - // playbookRunRefIndexField is the field index for looking up - // AgentPlaybookRuns by playbookRef. - playbookRunRefIndexField = ".spec.playbookRef" + // workflowRunRefIndexField is the field index for looking up + // AgentWorkflowRuns by workflowRef. + workflowRunRefIndexField = ".spec.workflowRef" ) -// AgentPlaybookRunReconciler reconciles an AgentPlaybookRun object. -type AgentPlaybookRunReconciler struct { +// AgentWorkflowRunReconciler reconciles an AgentWorkflowRun object. +type AgentWorkflowRunReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder events.EventRecorder } -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns/finalizers,verbs=update -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks,verbs=get;list;watch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns/finalizers,verbs=update +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows,verbs=get;list;watch // +kubebuilder:rbac:groups=konveyor.io,resources=agentruns,verbs=get;list;watch;create +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch -// Reconcile handles AgentPlaybookRun reconciliation. +// Reconcile handles AgentWorkflowRun reconciliation. // -// The controller orchestrates sequential execution of playbook stages: -// 1. Looks up the referenced AgentPlaybook +// The controller orchestrates sequential execution of workflow stages: +// 1. Looks up the referenced AgentWorkflow // 2. Determines the current stage from status // 3. Creates an AgentRun for the current stage if none exists // 4. Watches the AgentRun to completion -// 5. Advances to the next stage or marks the playbook run as complete -func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// 5. Advances to the next stage or marks the workflow run as complete +func (r *AgentWorkflowRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - var pbRun konveyoriov1alpha1.AgentPlaybookRun + var pbRun konveyoriov1alpha1.AgentWorkflowRun if err := r.Get(ctx, req.NamespacedName, &pbRun); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - logger.V(1).Info("Reconciling AgentPlaybookRun", "name", pbRun.Name) + logger.V(1).Info("Reconciling AgentWorkflowRun", "name", pbRun.Name) original := pbRun.DeepCopy() pbRun.Status.ObservedGeneration = pbRun.Generation @@ -80,10 +85,10 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } - // Look up the referenced AgentPlaybook. - var playbook konveyoriov1alpha1.AgentPlaybook - playbookKey := types.NamespacedName{Namespace: pbRun.Namespace, Name: pbRun.Spec.PlaybookRef} - if err := r.Get(ctx, playbookKey, &playbook); err != nil { + // Look up the referenced AgentWorkflow. + var workflow konveyoriov1alpha1.AgentWorkflow + workflowKey := types.NamespacedName{Namespace: pbRun.Namespace, Name: pbRun.Spec.WorkflowRef} + if err := r.Get(ctx, workflowKey, &workflow); err != nil { if errors.IsNotFound(err) { pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() @@ -92,23 +97,23 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req Type: ConditionTypeReady, Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, - Reason: "PlaybookNotFound", - Message: fmt.Sprintf("AgentPlaybook %q not found", pbRun.Spec.PlaybookRef), + Reason: "WorkflowNotFound", + Message: fmt.Sprintf("AgentWorkflow %q not found", pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } return ctrl.Result{}, err } - // Check that the playbook is Ready. - playbookReady := meta.FindStatusCondition(playbook.Status.Conditions, ConditionTypeReady) - if playbookReady == nil || playbookReady.Status != metav1.ConditionTrue { + // Check that the workflow is Ready. + workflowReady := meta.FindStatusCondition(workflow.Status.Conditions, ConditionTypeReady) + if workflowReady == nil || workflowReady.Status != metav1.ConditionTrue { meta.SetStatusCondition(&pbRun.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, - Reason: "PlaybookNotReady", - Message: fmt.Sprintf("AgentPlaybook %q is not Ready", pbRun.Spec.PlaybookRef), + Reason: "WorkflowNotReady", + Message: fmt.Sprintf("AgentWorkflow %q is not Ready", pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } @@ -122,9 +127,9 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // Initialize stage statuses if empty. if len(pbRun.Status.Stages) == 0 { - pbRun.Status.Stages = make([]konveyoriov1alpha1.AgentPlaybookRunStageStatus, len(playbook.Spec.Stages)) - for i, stage := range playbook.Spec.Stages { - pbRun.Status.Stages[i] = konveyoriov1alpha1.AgentPlaybookRunStageStatus{ + pbRun.Status.Stages = make([]konveyoriov1alpha1.AgentWorkflowRunStageStatus, len(workflow.Spec.Stages)) + for i, stage := range workflow.Spec.Stages { + pbRun.Status.Stages[i] = konveyoriov1alpha1.AgentWorkflowRunStageStatus{ Name: stage.Name, Phase: konveyoriov1alpha1.AgentRunPhasePending, } @@ -132,7 +137,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req } // Find the current stage to process. Use the snapshotted status - // stages as the source of truth — the playbook could have been + // stages as the source of truth — the workflow could have been // modified since the run started, but the run executes the stages // that were captured at initialization time. stageIndex := r.findCurrentStageIndex(&pbRun) @@ -152,18 +157,18 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return r.patchRunStatus(ctx, &pbRun, original) } - // Look up the stage definition from the playbook by name + // Look up the stage definition from the workflow by name // (matching the snapshotted status entry). stageStatus := &pbRun.Status.Stages[stageIndex] - var stage *konveyoriov1alpha1.AgentPlaybookStage - for i := range playbook.Spec.Stages { - if playbook.Spec.Stages[i].Name == stageStatus.Name { - stage = &playbook.Spec.Stages[i] + var stage *konveyoriov1alpha1.AgentWorkflowStage + for i := range workflow.Spec.Stages { + if workflow.Spec.Stages[i].Name == stageStatus.Name { + stage = &workflow.Spec.Stages[i] break } } if stage == nil { - // The playbook was modified and no longer has this stage. + // The workflow was modified and no longer has this stage. pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() pbRun.Status.CompletionTime = &now @@ -172,7 +177,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, Reason: "StageNotFound", - Message: fmt.Sprintf("Stage %q no longer exists in AgentPlaybook %q", stageStatus.Name, pbRun.Spec.PlaybookRef), + Message: fmt.Sprintf("Stage %q no longer exists in AgentWorkflow %q", stageStatus.Name, pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } @@ -182,7 +187,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // If no AgentRun exists for this stage, create one. if stageStatus.AgentRunName == "" { - agentRunName, err := r.createAgentRunForStage(ctx, &pbRun, &playbook, stage) + agentRunName, err := r.createAgentRunForStage(ctx, &pbRun, &workflow, stage, stageIndex, len(pbRun.Status.Stages)) if err != nil { logger.Error(err, "Failed to create AgentRun for stage", "stage", stage.Name) @@ -248,7 +253,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return r.patchRunStatus(ctx, &pbRun, original) case konveyoriov1alpha1.AgentRunPhaseFailed: - // Stage failed — fail the entire playbook run. + // Stage failed — fail the entire workflow run. pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() pbRun.Status.CompletionTime = &now @@ -276,8 +281,8 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // findCurrentStageIndex returns the index of the first stage that has not // yet succeeded. Returns len(stages) if all stages have succeeded. -func (r *AgentPlaybookRunReconciler) findCurrentStageIndex( - pbRun *konveyoriov1alpha1.AgentPlaybookRun, +func (r *AgentWorkflowRunReconciler) findCurrentStageIndex( + pbRun *konveyoriov1alpha1.AgentWorkflowRun, ) int { for i, stage := range pbRun.Status.Stages { if stage.Phase != konveyoriov1alpha1.AgentRunPhaseSucceeded { @@ -294,33 +299,92 @@ func stageAgentRunName(pbRunName, stageName string) string { return sanitizeVolumeName(pbRunName + "-" + stageName) } -// createAgentRunForStage creates an AgentRun for the given playbook stage. -// It forwards params, models, env, and envFrom from the playbook run spec. -// Playbook-level instructions (Guide) are passed as a separate env var +// createAgentRunForStage creates an AgentRun for the given workflow stage. +// It forwards models, env, and envFrom from the workflow run spec. Params +// are filtered to only those the stage's Agent declares — this avoids +// forcing every stage Agent to declare every param from other stages. +// Workflow-level instructions (Guide) are passed as a separate env var // so the harness can present them alongside stage instructions without // the controller making prompt composition decisions. // -// Uses a deterministic name (-) so that duplicate +// Uses a deterministic name (-) so that duplicate // creation on status-patch conflict is caught by AlreadyExists. -func (r *AgentPlaybookRunReconciler) createAgentRunForStage( +func (r *AgentWorkflowRunReconciler) createAgentRunForStage( ctx context.Context, - pbRun *konveyoriov1alpha1.AgentPlaybookRun, - playbook *konveyoriov1alpha1.AgentPlaybook, - stage *konveyoriov1alpha1.AgentPlaybookStage, + pbRun *konveyoriov1alpha1.AgentWorkflowRun, + workflow *konveyoriov1alpha1.AgentWorkflow, + stage *konveyoriov1alpha1.AgentWorkflowStage, + stageIndex int, + stageCount int, ) (string, error) { agentRunName := stageAgentRunName(pbRun.Name, stage.Name) - // Pass playbook-level instructions (Guide) as an env var. - // The harness decides how to compose this with the Agent prompt - // and stage instructions. + // Look up the stage's Agent to determine which params it declares. + var agent konveyoriov1alpha1.Agent + if err := r.Get(ctx, types.NamespacedName{ + Name: stage.AgentRef, Namespace: pbRun.Namespace, + }, &agent); err != nil { + return "", fmt.Errorf("looking up Agent %q for stage %q: %w", stage.AgentRef, stage.Name, err) + } + + // Build a set of param names the stage Agent declares. + declared := make(map[string]bool, len(agent.Spec.Params)) + for _, p := range agent.Spec.Params { + declared[p.Name] = true + } + + // Filter workflow-run params to only those this stage's Agent declares. + // Params not declared by the stage Agent are silently dropped — log + // and emit an event so typos are debuggable. + var stageParams []konveyoriov1alpha1.AgentRunParam + var skipped []string + for _, p := range pbRun.Spec.Params { + if declared[p.Name] { + stageParams = append(stageParams, p) + } else { + skipped = append(skipped, p.Name) + } + } + if len(skipped) > 0 { + logger := log.FromContext(ctx) + logger.V(1).Info("Filtered undeclared params for stage", + "stage", stage.Name, + "agent", stage.AgentRef, + "skippedParams", skipped, + ) + r.Recorder.Eventf(pbRun, nil, corev1.EventTypeNormal, "ParamsFiltered", + "FilterParams", "Stage %q (Agent %q): skipped undeclared params: %s", + stage.Name, stage.AgentRef, strings.Join(skipped, ", ")) + } + + // User-supplied env vars first, then controller-owned vars last. + // Kubernetes uses last-entry-wins for duplicate names, so + // controller-injected vars cannot be overridden by user input. var env []corev1.EnvVar - if playbook.Spec.Guide != "" { + env = append(env, pbRun.Spec.Env...) + + // Controller-owned env vars — appended after user env so they + // cannot be spoofed. + if workflow.Spec.Guide != "" { env = append(env, corev1.EnvVar{ - Name: "KONVEYOR_PLAYBOOK_INSTRUCTIONS", - Value: playbook.Spec.Guide, + Name: "KONVEYOR_WORKFLOW_GUIDE", + Value: workflow.Spec.Guide, }) } - env = append(env, pbRun.Spec.Env...) + + // Stage metadata for the harness. Used for stage-aware token + // revocation: the harness revokes the Hub API token only on the + // last stage (#68). + env = append(env, + corev1.EnvVar{ + Name: "KONVEYOR_WORKFLOW_STAGE", + Value: fmt.Sprintf("%d", stageIndex+1), + }, + corev1.EnvVar{ + Name: "KONVEYOR_WORKFLOW_STAGE_COUNT", + Value: fmt.Sprintf("%d", stageCount), + }, + ) agentRun := &konveyoriov1alpha1.AgentRun{ ObjectMeta: metav1.ObjectMeta{ @@ -328,7 +392,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( Namespace: pbRun.Namespace, Labels: map[string]string{ labelManagedBy: managedByLabel, - labelAgentPlaybookRun: pbRun.Name, + labelAgentWorkflowRun: pbRun.Name, labelStage: stage.Name, }, }, @@ -336,7 +400,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( AgentRef: stage.AgentRef, Instructions: stage.Instructions, Models: pbRun.Spec.Models, - Params: pbRun.Spec.Params, + Params: stageParams, Env: env, EnvFrom: pbRun.Spec.EnvFrom, }, @@ -349,7 +413,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( if err := r.Create(ctx, agentRun); err != nil { if errors.IsAlreadyExists(err) { // AgentRun was likely created on a prior reconcile but the - // status patch failed. Verify it belongs to this playbook + // status patch failed. Verify it belongs to this workflow // run before accepting it. var existing konveyoriov1alpha1.AgentRun if getErr := r.Get(ctx, types.NamespacedName{ @@ -358,7 +422,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( return "", fmt.Errorf("fetching existing AgentRun %q: %w", agentRunName, getErr) } if !isOwnedBy(&existing, pbRun) { - return "", fmt.Errorf("AgentRun %q already exists but is not owned by this playbook run", agentRunName) + return "", fmt.Errorf("AgentRun %q already exists but is not owned by this workflow run", agentRunName) } return agentRunName, nil } @@ -379,65 +443,65 @@ func isOwnedBy(child client.Object, parent client.Object) bool { return false } -// patchRunStatus patches the AgentPlaybookRun status. -func (r *AgentPlaybookRunReconciler) patchRunStatus( +// patchRunStatus patches the AgentWorkflowRun status. +func (r *AgentWorkflowRunReconciler) patchRunStatus( ctx context.Context, - pbRun *konveyoriov1alpha1.AgentPlaybookRun, - original *konveyoriov1alpha1.AgentPlaybookRun, + pbRun *konveyoriov1alpha1.AgentWorkflowRun, + original *konveyoriov1alpha1.AgentWorkflowRun, ) (ctrl.Result, error) { if err := r.Status().Patch(ctx, pbRun, client.MergeFrom(original)); err != nil { - log.FromContext(ctx).Error(err, "Failed to patch AgentPlaybookRun status", - "agentPlaybookRun", pbRun.Name) + log.FromContext(ctx).Error(err, "Failed to patch AgentWorkflowRun status", + "agentWorkflowRun", pbRun.Name) return ctrl.Result{}, err } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. -func (r *AgentPlaybookRunReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Index AgentPlaybookRuns by playbookRef for efficient reverse lookup - // when an AgentPlaybook changes. +func (r *AgentWorkflowRunReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Index AgentWorkflowRuns by workflowRef for efficient reverse lookup + // when an AgentWorkflow changes. if err := mgr.GetFieldIndexer().IndexField( context.Background(), - &konveyoriov1alpha1.AgentPlaybookRun{}, - playbookRunRefIndexField, + &konveyoriov1alpha1.AgentWorkflowRun{}, + workflowRunRefIndexField, func(obj client.Object) []string { - pbRun := obj.(*konveyoriov1alpha1.AgentPlaybookRun) - return []string{pbRun.Spec.PlaybookRef} + pbRun := obj.(*konveyoriov1alpha1.AgentWorkflowRun) + return []string{pbRun.Spec.WorkflowRef} }, ); err != nil { - return fmt.Errorf("indexing %s: %w", playbookRunRefIndexField, err) + return fmt.Errorf("indexing %s: %w", workflowRunRefIndexField, err) } return ctrl.NewControllerManagedBy(mgr). - For(&konveyoriov1alpha1.AgentPlaybookRun{}). + For(&konveyoriov1alpha1.AgentWorkflowRun{}). Owns(&konveyoriov1alpha1.AgentRun{}). Watches( - &konveyoriov1alpha1.AgentPlaybook{}, - handler.EnqueueRequestsFromMapFunc(r.findRunsForPlaybook), + &konveyoriov1alpha1.AgentWorkflow{}, + handler.EnqueueRequestsFromMapFunc(r.findRunsForWorkflow), ). - Named("agentplaybookrun"). + Named("agentworkflowrun"). Complete(r) } -// findRunsForPlaybook returns reconcile requests for all non-terminal -// AgentPlaybookRuns that reference the given AgentPlaybook. -func (r *AgentPlaybookRunReconciler) findRunsForPlaybook( +// findRunsForWorkflow returns reconcile requests for all non-terminal +// AgentWorkflowRuns that reference the given AgentWorkflow. +func (r *AgentWorkflowRunReconciler) findRunsForWorkflow( ctx context.Context, obj client.Object, ) []reconcile.Request { - playbook, ok := obj.(*konveyoriov1alpha1.AgentPlaybook) + workflow, ok := obj.(*konveyoriov1alpha1.AgentWorkflow) if !ok { return nil } - var runList konveyoriov1alpha1.AgentPlaybookRunList + var runList konveyoriov1alpha1.AgentWorkflowRunList if err := r.List(ctx, &runList, - client.InNamespace(playbook.Namespace), - client.MatchingFields{playbookRunRefIndexField: playbook.Name}, + client.InNamespace(workflow.Namespace), + client.MatchingFields{workflowRunRefIndexField: workflow.Name}, ); err != nil { - log.FromContext(ctx).Error(err, "Failed to list AgentPlaybookRuns for AgentPlaybook", - "playbook", playbook.Name) + log.FromContext(ctx).Error(err, "Failed to list AgentWorkflowRuns for AgentWorkflow", + "workflow", workflow.Name) return nil } diff --git a/internal/controller/agentplaybookrun_controller_test.go b/internal/controller/agentworkflowrun_controller_test.go similarity index 58% rename from internal/controller/agentplaybookrun_controller_test.go rename to internal/controller/agentworkflowrun_controller_test.go index 1d46fd3c..31a56c6e 100644 --- a/internal/controller/agentplaybookrun_controller_test.go +++ b/internal/controller/agentworkflowrun_controller_test.go @@ -44,11 +44,11 @@ func updateAgentRunStatus(name string, mutate func(*konveyoriov1alpha1.AgentRun) }, 10*time.Second, 250*time.Millisecond).Should(Succeed()) } -// waitForPlaybookReady waits until the named AgentPlaybook has Ready=True. -func waitForPlaybookReady(playbookName string) { - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} +// waitForWorkflowReady waits until the named AgentWorkflow has Ready=True. +func waitForWorkflowReady(workflowName string) { + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} EventuallyWithOffset(1, func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -56,41 +56,41 @@ func waitForPlaybookReady(playbookName string) { }, 10*time.Second, 250*time.Millisecond).Should(Succeed()) } -var _ = Describe("AgentPlaybookRun Controller", func() { +var _ = Describe("AgentWorkflowRun Controller", func() { const ( timeout = 10 * time.Second interval = 250 * time.Millisecond ) - Context("when the referenced AgentPlaybook does not exist", func() { - const name = "apr-ctrl-no-playbook" + Context("when the referenced AgentWorkflow does not exist", func() { + const name = "apr-ctrl-no-workflow" - It("should set Phase=Failed with PlaybookNotFound", func() { - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + It("should set Phase=Failed with WorkflowNotFound", func() { + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "nonexistent-playbook", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "nonexistent-workflow", }, } Expect(k8sClient.Create(ctx, pbRun)).To(Succeed()) key := types.NamespacedName{Name: name, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseFailed)) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) - g.Expect(readyCond.Reason).To(Equal("PlaybookNotFound")) + g.Expect(readyCond.Reason).To(Equal("WorkflowNotFound")) }, timeout, interval).Should(Succeed()) Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) }) }) - Context("when the playbook is valid and stages execute sequentially", func() { + Context("when the workflow is valid and stages execute sequentially", func() { const ( - playbookName = "apr-ctrl-seq-playbook" + workflowName = "apr-ctrl-seq-workflow" pbRunName = "apr-ctrl-seq-run" agentName = "apr-ctrl-seq-agent" provName = "apr-prov-seq" @@ -115,25 +115,25 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(k8sClient.Create(ctx, agent)).To(Succeed()) waitForAgentReady(agentName) - By("creating a Ready AgentPlaybook with two stages") - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Guide: "Sequential test playbook", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + By("creating a Ready AgentWorkflow with two stages") + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Guide: "Sequential test workflow", + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "stage-a", AgentRef: agentName, Instructions: "Do stage A"}, {Name: "stage-b", AgentRef: agentName, Instructions: "Do stage B"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) - waitForPlaybookReady(playbookName) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) - By("creating the AgentPlaybookRun") - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + By("creating the AgentWorkflowRun") + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: playbookName, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, }, @@ -148,7 +148,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} expectedStageAName := stageAgentRunName(pbRunName, "stage-a") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseRunning)) g.Expect(fetched.Status.CurrentStage).To(Equal("stage-a")) @@ -171,11 +171,11 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(stageARun.Spec.Models[0].Role).To(Equal(testRolePrimary)) By("verifying stage-a AgentRun has correct labels") - Expect(stageARun.Labels).To(HaveKeyWithValue(labelAgentPlaybookRun, pbRunName)) + Expect(stageARun.Labels).To(HaveKeyWithValue(labelAgentWorkflowRun, pbRunName)) Expect(stageARun.Labels).To(HaveKeyWithValue(labelStage, "stage-a")) By("verifying stage-b is not started yet") - var fetchedPBRun konveyoriov1alpha1.AgentPlaybookRun + var fetchedPBRun konveyoriov1alpha1.AgentWorkflowRun Expect(k8sClient.Get(ctx, pbRunKey, &fetchedPBRun)).To(Succeed()) Expect(fetchedPBRun.Status.Stages[1].AgentRunName).To(BeEmpty()) Expect(fetchedPBRun.Status.Stages[1].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhasePending)) @@ -193,7 +193,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { By("verifying stage-b AgentRun is created") var stageBRunName string Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.CurrentStage).To(Equal("stage-b")) g.Expect(fetched.Status.Stages[0].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseSucceeded)) @@ -219,9 +219,9 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }) }) - By("verifying the playbook run completes successfully") + By("verifying the workflow run completes successfully") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseSucceeded)) g.Expect(fetched.Status.CompletionTime).NotTo(BeNil()) @@ -236,27 +236,159 @@ var _ = Describe("AgentPlaybookRun Controller", func() { var runList konveyoriov1alpha1.AgentRunList Expect(k8sClient.List(ctx, &runList, client.InNamespace(testNamespace), - client.MatchingLabels{labelAgentPlaybookRun: pbRunName}, + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, )).To(Succeed()) for i := range runList.Items { Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) } Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent)).To(Succeed()) }) }) + Context("when stages use different Agents with different params", func() { + const ( + workflowName = "apr-ctrl-filter-workflow" + pbRunName = "apr-ctrl-filter-run" + agentAName = "apr-ctrl-filter-agent-a" + agentBName = "apr-ctrl-filter-agent-b" + provName = "apr-prov-filter" + secretName = "apr-secret-filter" + ) + + It("should forward only params each stage Agent declares", func() { + cleanup := makeReadyProvider(provName, secretName) + defer cleanup() + + By("creating Agent A that declares 'source_url' only") + agentA := &konveyoriov1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentAName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentSpec{ + Image: testAgentImage, + Providers: []konveyoriov1alpha1.AgentProviderRef{{Ref: provName}}, + Params: []konveyoriov1alpha1.AgentParam{ + {Name: "source_url", Required: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, agentA)).To(Succeed()) + waitForAgentReady(agentAName) + + By("creating Agent B that declares 'target_branch' only") + agentB := &konveyoriov1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentBName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentSpec{ + Image: testAgentImage, + Providers: []konveyoriov1alpha1.AgentProviderRef{{Ref: provName}}, + Params: []konveyoriov1alpha1.AgentParam{ + {Name: testParamTargetBranch, Required: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, agentB)).To(Succeed()) + waitForAgentReady(agentBName) + + By("creating a workflow with two stages using different Agents") + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ + {Name: "stage-a", AgentRef: agentAName}, + {Name: "stage-b", AgentRef: agentBName}, + }, + }, + } + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) + + By("creating the workflow run with params for both stages") + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ + ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, + Models: []konveyoriov1alpha1.AgentRunModelSelection{ + {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, + }, + Params: []konveyoriov1alpha1.AgentRunParam{ + {Name: "source_url", Value: "https://github.com/example/repo.git"}, + {Name: testParamTargetBranch, Value: "konveyor/test"}, + }, + }, + } + Expect(k8sClient.Create(ctx, pbRun)).To(Succeed()) + + By("verifying stage-a AgentRun gets only 'source_url'") + pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} + expectedStageAName := stageAgentRunName(pbRunName, "stage-a") + Eventually(func(g Gomega) { + var fetched konveyoriov1alpha1.AgentWorkflowRun + g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) + g.Expect(fetched.Status.Stages).To(HaveLen(2)) + g.Expect(fetched.Status.Stages[0].AgentRunName).To(Equal(expectedStageAName)) + }, timeout, interval).Should(Succeed()) + + var stageARun konveyoriov1alpha1.AgentRun + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: expectedStageAName, Namespace: testNamespace, + }, &stageARun)).To(Succeed()) + Expect(stageARun.Spec.Params).To(HaveLen(1)) + Expect(stageARun.Spec.Params[0].Name).To(Equal("source_url")) + + By("simulating stage-a success to advance to stage-b") + updateAgentRunStatus(expectedStageAName, func(run *konveyoriov1alpha1.AgentRun) { + run.Status.Phase = konveyoriov1alpha1.AgentRunPhaseSucceeded + now := metav1.Now() + run.Status.CompletionTime = &now + meta.SetStatusCondition(&run.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: reasonSucceeded, + }) + }) + + By("verifying stage-b AgentRun gets only 'target_branch'") + expectedStageBName := stageAgentRunName(pbRunName, "stage-b") + Eventually(func(g Gomega) { + var fetched konveyoriov1alpha1.AgentWorkflowRun + g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) + g.Expect(fetched.Status.Stages).To(HaveLen(2)) + g.Expect(fetched.Status.Stages[1].AgentRunName).To(Equal(expectedStageBName)) + }, timeout, interval).Should(Succeed()) + + var stageBRun konveyoriov1alpha1.AgentRun + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: expectedStageBName, Namespace: testNamespace, + }, &stageBRun)).To(Succeed()) + Expect(stageBRun.Spec.Params).To(HaveLen(1)) + Expect(stageBRun.Spec.Params[0].Name).To(Equal(testParamTargetBranch)) + + By("cleaning up") + var runList konveyoriov1alpha1.AgentRunList + Expect(k8sClient.List(ctx, &runList, + client.InNamespace(testNamespace), + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, + )).To(Succeed()) + for i := range runList.Items { + Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) + } + Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) + Expect(k8sClient.Delete(ctx, agentA)).To(Succeed()) + Expect(k8sClient.Delete(ctx, agentB)).To(Succeed()) + }) + }) + Context("when a stage fails", func() { const ( - playbookName = "apr-ctrl-fail-playbook" + workflowName = "apr-ctrl-fail-workflow" pbRunName = "apr-ctrl-fail-run" agentName = "apr-ctrl-fail-agent" provName = "apr-prov-fail" secretName = "apr-secret-fail" ) - It("should fail the entire playbook run", func() { + It("should fail the entire workflow run", func() { cleanup := makeReadyProvider(provName, secretName) defer cleanup() @@ -270,22 +402,22 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(k8sClient.Create(ctx, agent)).To(Succeed()) waitForAgentReady(agentName) - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "will-fail", AgentRef: agentName, Instructions: "This will fail"}, {Name: "never-runs", AgentRef: agentName, Instructions: "Should not run"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) - waitForPlaybookReady(playbookName) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: playbookName, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, }, @@ -297,7 +429,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} var stageRunName string Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Stages).To(HaveLen(2)) g.Expect(fetched.Status.Stages[0].AgentRunName).NotTo(BeEmpty()) @@ -314,9 +446,9 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }) }) - By("verifying the playbook run fails") + By("verifying the workflow run fails") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseFailed)) g.Expect(fetched.Status.CompletionTime).NotTo(BeNil()) @@ -326,23 +458,23 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }, timeout, interval).Should(Succeed()) By("verifying stage-2 was never started") - var finalPBRun konveyoriov1alpha1.AgentPlaybookRun + var finalPBRun konveyoriov1alpha1.AgentWorkflowRun Expect(k8sClient.Get(ctx, pbRunKey, &finalPBRun)).To(Succeed()) Expect(finalPBRun.Status.Stages[1].AgentRunName).To(BeEmpty()) Expect(finalPBRun.Status.Stages[1].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhasePending)) - // Clean up — delete the AgentRuns owned by the playbook run + // Clean up — delete the AgentRuns owned by the workflow run // first to avoid GC issues in tests. var runList konveyoriov1alpha1.AgentRunList Expect(k8sClient.List(ctx, &runList, client.InNamespace(testNamespace), - client.MatchingLabels{labelAgentPlaybookRun: pbRunName}, + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, )).To(Succeed()) for i := range runList.Items { Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) } Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent)).To(Succeed()) }) }) diff --git a/internal/controller/crd_validation_test.go b/internal/controller/crd_validation_test.go index 94f8fa20..a470e0d7 100644 --- a/internal/controller/crd_validation_test.go +++ b/internal/controller/crd_validation_test.go @@ -30,10 +30,11 @@ import ( ) const ( - testImageGoose = "quay.io/konveyor/agent-java-goose:latest" - testParamName = "source_url" - testProvider = "anthropic-provider" - testModel = "claude-sonnet-4-20250514" + testImageGoose = "quay.io/konveyor/agent-java-goose:latest" + testParamName = "source_url" + testParamTargetBranch = "target_branch" + testProvider = "anthropic-provider" + testModel = "claude-sonnet-4-20250514" ) var _ = Describe("CRD Validation", func() { @@ -236,7 +237,7 @@ var _ = Describe("CRD Validation", func() { }, Params: []konveyoriov1alpha1.AgentParam{ { - Name: "target_branch", + Name: testParamTargetBranch, Default: testDefaultBranch, // required is omitted — this must not fail }, @@ -376,17 +377,17 @@ var _ = Describe("CRD Validation", func() { }) }) - // ── AgentPlaybook ────────────────────────────────────────────────── - Context("AgentPlaybook", func() { - It("should accept a valid AgentPlaybook", func() { - ap := &konveyoriov1alpha1.AgentPlaybook{ + // ── AgentWorkflow ────────────────────────────────────────────────── + Context("AgentWorkflow", func() { + It("should accept a valid AgentWorkflow", func() { + ap := &konveyoriov1alpha1.AgentWorkflow{ ObjectMeta: metav1.ObjectMeta{ Name: "ap-valid-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Guide: "Migrate a Java EE application to Quarkus.", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "discover", AgentRef: "discovery-agent", Instructions: "Analyze the app."}, {Name: "implement", AgentRef: "migration-agent", Instructions: "Execute migration."}, }, @@ -396,15 +397,15 @@ var _ = Describe("CRD Validation", func() { Expect(k8sClient.Delete(ctx, ap)).To(Succeed()) }) - It("should reject an AgentPlaybook with empty stages", func() { - ap := &konveyoriov1alpha1.AgentPlaybook{ + It("should reject an AgentWorkflow with empty stages", func() { + ap := &konveyoriov1alpha1.AgentWorkflow{ ObjectMeta: metav1.ObjectMeta{ Name: "ap-empty-stages-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Guide: "No stages here.", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{}, + Stages: []konveyoriov1alpha1.AgentWorkflowStage{}, }, } err := k8sClient.Create(ctx, ap) @@ -458,16 +459,16 @@ var _ = Describe("CRD Validation", func() { }) }) - // ── AgentPlaybookRun ─────────────────────────────────────────────── - Context("AgentPlaybookRun", func() { - It("should accept a valid AgentPlaybookRun", func() { - apr := &konveyoriov1alpha1.AgentPlaybookRun{ + // ── AgentWorkflowRun ─────────────────────────────────────────────── + Context("AgentWorkflowRun", func() { + It("should accept a valid AgentWorkflowRun", func() { + apr := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{ Name: "apr-valid-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "java-migration", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "java-migration", Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: "anthropic", Model: testModel}, }, @@ -488,25 +489,25 @@ var _ = Describe("CRD Validation", func() { Expect(k8sClient.Delete(ctx, apr)).To(Succeed()) }) - It("should reject mutation of playbookRef", func() { - apr := &konveyoriov1alpha1.AgentPlaybookRun{ + It("should reject mutation of workflowRef", func() { + apr := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{ Name: "apr-immutable-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "original-playbook", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "original-workflow", }, } Expect(k8sClient.Create(ctx, apr)).To(Succeed()) - apr.Spec.PlaybookRef = "different-playbook" + apr.Spec.WorkflowRef = "different-workflow" err := k8sClient.Update(ctx, apr) Expect(err).To(HaveOccurred()) Expect(errors.IsInvalid(err)).To(BeTrue(), fmt.Sprintf("expected Invalid error, got: %v", err)) // Clean up - apr.Spec.PlaybookRef = "original-playbook" + apr.Spec.WorkflowRef = "original-workflow" Expect(k8sClient.Delete(ctx, apr)).To(Succeed()) }) }) diff --git a/internal/controller/doc.go b/internal/controller/doc.go index a1fe4de9..6bc9b04a 100644 --- a/internal/controller/doc.go +++ b/internal/controller/doc.go @@ -33,10 +33,10 @@ const ( // labelAgent identifies resources belonging to an Agent. labelAgent = "konveyor.io/agent" - // labelAgentPlaybookRun identifies resources belonging to an AgentPlaybookRun. - labelAgentPlaybookRun = "konveyor.io/agentplaybookrun" + // labelAgentWorkflowRun identifies resources belonging to an AgentWorkflowRun. + labelAgentWorkflowRun = "konveyor.io/agentworkflowrun" - // labelStage identifies the playbook stage a resource belongs to. + // labelStage identifies the workflow stage a resource belongs to. labelStage = "konveyor.io/stage" // reasonSucceeded is the condition reason for successful completion. diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index ba84623c..942a7b78 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -148,15 +148,16 @@ var _ = BeforeSuite(func() { }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&AgentPlaybookReconciler{ + err = (&AgentWorkflowReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&AgentPlaybookRunReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + err = (&AgentWorkflowRunReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("agentworkflowrun-controller"), }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred())