Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changes/unreleased/107-stage-label-inheritance.yaml
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 13 additions & 5 deletions internal/controller/agentworkflowrun_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package controller
import (
"context"
"fmt"
"maps"

"strings"

Expand Down Expand Up @@ -386,15 +387,22 @@ 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)
maps.Copy(labels, pbRun.Labels)
labels[labelManagedBy] = managedByLabel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first usage of maps.Copy in the codebase — the rest of the controller package uses inline map[string]string{...} literals or manual assignment. maps.Copy is the idiomatic Go 1.21+ way to do this. We should file an issue to update the rest of the codebase to use this pattern.

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,
Expand Down
109 changes: 101 additions & 8 deletions internal/controller/agentworkflowrun_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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"},
},
},
Expand All @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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},
},
},
Expand All @@ -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())
Expand Down Expand Up @@ -374,6 +375,98 @@ var _ = Describe("AgentWorkflowRun Controller", func() {
})
})

Context("when the workflow run carries caller-supplied labels", func() {
const (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test covers propagation (caller labels appear on the stage run) and spoofing (controller-owned keys win). One gap: there's no explicit assertion for the nil-labels case — when the parent AgentWorkflowRun has no labels. The existing sequential test creates a workflow run without explicit labels, implicitly covering this path, but doesn't assert Expect(stageARun.Labels).To(HaveLen(3)) to make the contract explicit. Not blocking — maps.Copy with nil is a no-op — but an explicit assertion would be stronger.

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: stageAName, 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, stageAName)
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, stageAName))

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"
Expand Down
Loading