From fc93e2ed3f7a235898d9854ef8fecd3df0cd2d66 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Fri, 7 Aug 2026 09:29:29 -0400 Subject: [PATCH 1/2] :bug: Stage AgentRuns inherit the parent AgentWorkflowRun's labels Stage runs were created with a fixed label map, so caller labels on the workflow run (e.g. konveyor.io/application per ADR 0006) never reached the runs that execute, and label-selector queries silently missed them. Propagate all parent labels; controller-owned keys are written last. Fixes #107 Co-Authored-By: Claude Fable 5 Signed-off-by: ibolton336 --- .../107-stage-label-inheritance.yaml | 6 ++ .../controller/agentworkflowrun_controller.go | 19 +++- .../agentworkflowrun_controller_test.go | 92 +++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 changes/unreleased/107-stage-label-inheritance.yaml diff --git a/changes/unreleased/107-stage-label-inheritance.yaml b/changes/unreleased/107-stage-label-inheritance.yaml new file mode 100644 index 00000000..0abd6893 --- /dev/null +++ b/changes/unreleased/107-stage-label-inheritance.yaml @@ -0,0 +1,6 @@ +kind: bugfix + +description: > + Stage AgentRuns now inherit all of the parent AgentWorkflowRun's labels + (controller-owned keys still win), so label-selector queries such as + konveyor.io/application match the runs that actually execute. diff --git a/internal/controller/agentworkflowrun_controller.go b/internal/controller/agentworkflowrun_controller.go index d0fd634c..e9e971bb 100644 --- a/internal/controller/agentworkflowrun_controller.go +++ b/internal/controller/agentworkflowrun_controller.go @@ -386,15 +386,24 @@ func (r *AgentWorkflowRunReconciler) createAgentRunForStage( }, ) + // Stage AgentRuns inherit all of the workflow run's labels so + // label-selector queries (e.g. konveyor.io/application, ADR 0006) + // match the runs that actually execute. Controller-owned keys are + // written last into a copy so callers cannot override them and the + // parent's live label map is never mutated. + labels := make(map[string]string, len(pbRun.Labels)+3) + for k, v := range pbRun.Labels { + labels[k] = v + } + labels[labelManagedBy] = managedByLabel + labels[labelAgentWorkflowRun] = pbRun.Name + labels[labelStage] = stage.Name + agentRun := &konveyoriov1alpha1.AgentRun{ ObjectMeta: metav1.ObjectMeta{ Name: agentRunName, Namespace: pbRun.Namespace, - Labels: map[string]string{ - labelManagedBy: managedByLabel, - labelAgentWorkflowRun: pbRun.Name, - labelStage: stage.Name, - }, + Labels: labels, }, Spec: konveyoriov1alpha1.AgentRunSpec{ AgentRef: stage.AgentRef, diff --git a/internal/controller/agentworkflowrun_controller_test.go b/internal/controller/agentworkflowrun_controller_test.go index 2d6a5b3e..e308a37c 100644 --- a/internal/controller/agentworkflowrun_controller_test.go +++ b/internal/controller/agentworkflowrun_controller_test.go @@ -374,6 +374,98 @@ var _ = Describe("AgentWorkflowRun Controller", func() { }) }) + Context("when the workflow run carries caller-supplied labels", func() { + const ( + workflowName = "apr-ctrl-labels-workflow" + pbRunName = "apr-ctrl-labels-run" + agentName = "apr-ctrl-labels-agent" + gwName = "apr-prov-labels" + secretName = "apr-secret-labels" + ) + + It("should propagate parent labels to stage AgentRuns with controller-owned keys winning", func() { + cleanup := makeReadyGateway(gwName, secretName) + defer cleanup() + + agent := &konveyoriov1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentSpec{ + Image: testAgentImage, + Gateways: []konveyoriov1alpha1.AgentGatewayRef{{Ref: gwName}}, + }, + } + Expect(k8sClient.Create(ctx, agent)).To(Succeed()) + waitForAgentReady(agentName) + + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ + {Name: "stage-a", AgentRef: agentName, Instructions: "Do stage A"}, + }, + }, + } + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) + + By("creating the workflow run with caller labels and spoofed controller-owned keys") + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: pbRunName, + Namespace: testNamespace, + Labels: map[string]string{ + "konveyor.io/application": "42", + "custom/foo": "bar", + labelManagedBy: "spoofed-manager", + labelAgentWorkflowRun: "spoofed-run", + labelStage: "spoofed-stage", + }, + }, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, + Gateway: gwName, + }, + } + Expect(k8sClient.Create(ctx, pbRun)).To(Succeed()) + + By("waiting for the stage AgentRun to be created") + pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} + expectedStageName := 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(1)) + g.Expect(fetched.Status.Stages[0].AgentRunName).To(Equal(expectedStageName)) + }, timeout, interval).Should(Succeed()) + + By("verifying the stage AgentRun inherits caller labels") + var stageRun konveyoriov1alpha1.AgentRun + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: expectedStageName, Namespace: testNamespace, + }, &stageRun)).To(Succeed()) + Expect(stageRun.Labels).To(HaveKeyWithValue("konveyor.io/application", "42")) + Expect(stageRun.Labels).To(HaveKeyWithValue("custom/foo", "bar")) + + By("verifying controller-owned keys keep controller values") + Expect(stageRun.Labels).To(HaveKeyWithValue(labelManagedBy, managedByLabel)) + Expect(stageRun.Labels).To(HaveKeyWithValue(labelAgentWorkflowRun, pbRunName)) + Expect(stageRun.Labels).To(HaveKeyWithValue(labelStage, "stage-a")) + + 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, agent)).To(Succeed()) + }) + }) + Context("when a stage fails", func() { const ( workflowName = "apr-ctrl-fail-workflow" From 35a8a9a65f05ecd6ab570bcd881aa892b452d6bd Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 12 Aug 2026 12:47:30 -0400 Subject: [PATCH 2/2] :rotating_light: Appease goconst and modernize linters maps.Copy for the label inheritance copy; hoist the thrice-used "stage-a" literal to a suite constant. Co-Authored-By: Claude Fable 5 Signed-off-by: ibolton336 --- .../controller/agentworkflowrun_controller.go | 5 ++-- .../agentworkflowrun_controller_test.go | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/controller/agentworkflowrun_controller.go b/internal/controller/agentworkflowrun_controller.go index e9e971bb..6b5a5ccc 100644 --- a/internal/controller/agentworkflowrun_controller.go +++ b/internal/controller/agentworkflowrun_controller.go @@ -19,6 +19,7 @@ package controller import ( "context" "fmt" + "maps" "strings" @@ -392,9 +393,7 @@ func (r *AgentWorkflowRunReconciler) createAgentRunForStage( // written last into a copy so callers cannot override them and the // parent's live label map is never mutated. labels := make(map[string]string, len(pbRun.Labels)+3) - for k, v := range pbRun.Labels { - labels[k] = v - } + maps.Copy(labels, pbRun.Labels) labels[labelManagedBy] = managedByLabel labels[labelAgentWorkflowRun] = pbRun.Name labels[labelStage] = stage.Name diff --git a/internal/controller/agentworkflowrun_controller_test.go b/internal/controller/agentworkflowrun_controller_test.go index e308a37c..2737a926 100644 --- a/internal/controller/agentworkflowrun_controller_test.go +++ b/internal/controller/agentworkflowrun_controller_test.go @@ -58,8 +58,9 @@ func waitForWorkflowReady(workflowName string) { var _ = Describe("AgentWorkflowRun Controller", func() { const ( - timeout = 10 * time.Second - interval = 250 * time.Millisecond + timeout = 10 * time.Second + interval = 250 * time.Millisecond + stageAName = "stage-a" ) Context("when the referenced AgentWorkflow does not exist", func() { @@ -121,7 +122,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Guide: "Sequential test workflow", Stages: []konveyoriov1alpha1.AgentWorkflowStage{ - {Name: "stage-a", AgentRef: agentName, Instructions: "Do stage A"}, + {Name: stageAName, AgentRef: agentName, Instructions: "Do stage A"}, {Name: "stage-b", AgentRef: agentName, Instructions: "Do stage B"}, }, }, @@ -144,12 +145,12 @@ var _ = Describe("AgentWorkflowRun Controller", func() { By("verifying stage-a AgentRun is created with deterministic name") pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} - expectedStageAName := stageAgentRunName(pbRunName, "stage-a") + expectedStageAName := stageAgentRunName(pbRunName, stageAName) Eventually(func(g Gomega) { 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")) + g.Expect(fetched.Status.CurrentStage).To(Equal(stageAName)) g.Expect(fetched.Status.Stages).To(HaveLen(2)) g.Expect(fetched.Status.Stages[0].AgentRunName).To(Equal(expectedStageAName)) }, timeout, interval).Should(Succeed()) @@ -169,7 +170,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { By("verifying stage-a AgentRun has correct labels") Expect(stageARun.Labels).To(HaveKeyWithValue(labelAgentWorkflowRun, pbRunName)) - Expect(stageARun.Labels).To(HaveKeyWithValue(labelStage, "stage-a")) + Expect(stageARun.Labels).To(HaveKeyWithValue(labelStage, stageAName)) By("verifying stage-b is not started yet") var fetchedPBRun konveyoriov1alpha1.AgentWorkflowRun @@ -291,7 +292,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Stages: []konveyoriov1alpha1.AgentWorkflowStage{ - {Name: "stage-a", AgentRef: agentAName}, + {Name: stageAName, AgentRef: agentAName}, {Name: "stage-b", AgentRef: agentBName}, }, }, @@ -315,7 +316,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { By("verifying stage-a AgentRun gets only 'source_url'") pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} - expectedStageAName := stageAgentRunName(pbRunName, "stage-a") + expectedStageAName := stageAgentRunName(pbRunName, stageAName) Eventually(func(g Gomega) { var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) @@ -401,7 +402,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Stages: []konveyoriov1alpha1.AgentWorkflowStage{ - {Name: "stage-a", AgentRef: agentName, Instructions: "Do stage A"}, + {Name: stageAName, AgentRef: agentName, Instructions: "Do stage A"}, }, }, } @@ -430,7 +431,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { By("waiting for the stage AgentRun to be created") pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} - expectedStageName := stageAgentRunName(pbRunName, "stage-a") + expectedStageName := stageAgentRunName(pbRunName, stageAName) Eventually(func(g Gomega) { var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) @@ -449,7 +450,7 @@ var _ = Describe("AgentWorkflowRun Controller", func() { By("verifying controller-owned keys keep controller values") Expect(stageRun.Labels).To(HaveKeyWithValue(labelManagedBy, managedByLabel)) Expect(stageRun.Labels).To(HaveKeyWithValue(labelAgentWorkflowRun, pbRunName)) - Expect(stageRun.Labels).To(HaveKeyWithValue(labelStage, "stage-a")) + Expect(stageRun.Labels).To(HaveKeyWithValue(labelStage, stageAName)) By("cleaning up") var runList konveyoriov1alpha1.AgentRunList