From f3388b1d6dded301bfef28fb45ed12aadb7e7ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Reme=C5=A1?= Date: Mon, 22 Jun 2026 12:35:07 +0200 Subject: [PATCH 1/2] fix: truncate proposal name in label values to 63 chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator uses proposal.Name directly as a Kubernetes label value (agentic.openshift.io/proposal) when creating result CRs, RBAC resources, sandbox pods, and SandboxClaim CRs. Kubernetes label values are limited to 63 characters, so names exceeding this limit cause resource creation failures. Apply truncateK8sName() to proposalName in all four label-setting locations as defense in depth. Signed-off-by: Tomáš Remeš Assisted-by: Claude Code:claude-opus-4-6 --- controller/proposal/bare_pod_manager.go | 2 +- controller/proposal/bare_pod_manager_test.go | 27 ++++++++++++++++++++ controller/proposal/rbac.go | 2 +- controller/proposal/rbac_test.go | 11 ++++++++ controller/proposal/results.go | 2 +- controller/proposal/results_test.go | 15 +++++++++++ controller/proposal/sandbox.go | 2 +- controller/proposal/sandbox_test.go | 14 ++++++++++ 8 files changed, 71 insertions(+), 4 deletions(-) diff --git a/controller/proposal/bare_pod_manager.go b/controller/proposal/bare_pod_manager.go index 3a7cf303..22bfab70 100644 --- a/controller/proposal/bare_pod_manager.go +++ b/controller/proposal/bare_pod_manager.go @@ -72,7 +72,7 @@ func (m *BarePodManager) Claim(ctx context.Context, proposalName, step, _ string Name: podName, Namespace: m.Namespace, Labels: map[string]string{ - LabelProposal: proposalName, + LabelProposal: truncateK8sName(proposalName), LabelStep: step, }, }, diff --git a/controller/proposal/bare_pod_manager_test.go b/controller/proposal/bare_pod_manager_test.go index 5c931001..38200e7d 100644 --- a/controller/proposal/bare_pod_manager_test.go +++ b/controller/proposal/bare_pod_manager_test.go @@ -2,6 +2,7 @@ package proposal import ( "context" + "strings" "testing" "time" @@ -79,6 +80,32 @@ func TestBarePodManager_Claim_UsesPerProposalSA(t *testing.T) { } } +func TestBarePodManager_Claim_TruncatesLongProposalNameInLabel(t *testing.T) { + fc := newBarePodClient().Build() + builder := &PodSpecBuilder{Image: "quay.io/test/sandbox:latest"} + m := NewBarePodManager(fc, builder, "test-ns") + m.SetStep( + &agenticv1alpha1.Agent{Spec: agenticv1alpha1.AgentSpec{Model: "claude-opus-4-6"}}, + testLLMProvider(agenticv1alpha1.LLMProviderAnthropic), + nil, + defaultSandboxSA, + ) + + longName := strings.Repeat("a", 80) + name, err := m.Claim(context.Background(), longName, "analysis", "") + if err != nil { + t.Fatalf("Claim: %v", err) + } + + var pod corev1.Pod + if err := fc.Get(context.Background(), types.NamespacedName{Name: name, Namespace: "test-ns"}, &pod); err != nil { + t.Fatalf("pod not created: %v", err) + } + if len(pod.Labels[LabelProposal]) > 63 { + t.Fatalf("proposal label length %d exceeds 63", len(pod.Labels[LabelProposal])) + } +} + func TestBarePodManager_Claim_AlreadyExists(t *testing.T) { existing := &corev1.Pod{} existing.Name = "ls-analysis-my-proposal" diff --git a/controller/proposal/rbac.go b/controller/proposal/rbac.go index 58d96bb9..0840374c 100644 --- a/controller/proposal/rbac.go +++ b/controller/proposal/rbac.go @@ -228,7 +228,7 @@ func clusterRoleName(proposalName string) string { func rbacLabels(proposalName, component string) map[string]string { return map[string]string{ - LabelProposal: proposalName, + LabelProposal: truncateK8sName(proposalName), LabelComponent: component, } } diff --git a/controller/proposal/rbac_test.go b/controller/proposal/rbac_test.go index a431d0a2..60aae96a 100644 --- a/controller/proposal/rbac_test.go +++ b/controller/proposal/rbac_test.go @@ -778,3 +778,14 @@ func TestRBACLabels(t *testing.T) { t.Fatalf("expected 2 labels, got %d", len(labels)) } } + +func TestRBACLabels_TruncatesLongProposalName(t *testing.T) { + longName := strings.Repeat("a", 80) + labels := rbacLabels(longName, "execution-rbac") + if len(labels[LabelProposal]) > 63 { + t.Fatalf("proposal label length %d exceeds 63", len(labels[LabelProposal])) + } + if labels[LabelProposal] != strings.Repeat("a", 63) { + t.Errorf("proposal label = %q, want %q", labels[LabelProposal], strings.Repeat("a", 63)) + } +} diff --git a/controller/proposal/results.go b/controller/proposal/results.go index 64390140..7dac766b 100644 --- a/controller/proposal/results.go +++ b/controller/proposal/results.go @@ -36,7 +36,7 @@ func proposalOwnerRef(proposal *agenticv1alpha1.Proposal) metav1.OwnerReference func resultLabels(proposalName, step string) map[string]string { return map[string]string{ - LabelProposal: proposalName, + LabelProposal: truncateK8sName(proposalName), LabelStep: step, } } diff --git a/controller/proposal/results_test.go b/controller/proposal/results_test.go index 2f993833..c153b171 100644 --- a/controller/proposal/results_test.go +++ b/controller/proposal/results_test.go @@ -2,6 +2,7 @@ package proposal import ( "context" + "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -11,6 +12,20 @@ import ( agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" ) +func TestResultLabels_TruncatesLongProposalName(t *testing.T) { + longName := strings.Repeat("a", 80) + labels := resultLabels(longName, "analysis") + if len(labels[LabelProposal]) > 63 { + t.Fatalf("proposal label length %d exceeds 63", len(labels[LabelProposal])) + } + if labels[LabelProposal] != strings.Repeat("a", 63) { + t.Errorf("proposal label = %q, want %q", labels[LabelProposal], strings.Repeat("a", 63)) + } + if labels[LabelStep] != "analysis" { + t.Errorf("step label = %q, want analysis", labels[LabelStep]) + } +} + func TestCreateIdempotent_StatusFieldsWritten(t *testing.T) { scheme := testScheme() fc := fake.NewClientBuilder().WithScheme(scheme). diff --git a/controller/proposal/sandbox.go b/controller/proposal/sandbox.go index 6dfe09a6..d5aec787 100644 --- a/controller/proposal/sandbox.go +++ b/controller/proposal/sandbox.go @@ -80,7 +80,7 @@ func (m *SandboxManager) buildClaim(claimName, proposalName, step, templateName "name": claimName, "namespace": m.Namespace, "labels": map[string]any{ - LabelProposal: proposalName, + LabelProposal: truncateK8sName(proposalName), LabelStep: step, }, }, diff --git a/controller/proposal/sandbox_test.go b/controller/proposal/sandbox_test.go index 8773f68c..91fcf3a2 100644 --- a/controller/proposal/sandbox_test.go +++ b/controller/proposal/sandbox_test.go @@ -107,6 +107,20 @@ func TestBuildClaim_Labels(t *testing.T) { } } +func TestBuildClaim_TruncatesLongProposalName(t *testing.T) { + longName := strings.Repeat("a", 80) + m := NewSandboxManager(nil, "ns", "") + claim := m.buildClaim("c", longName, "execution", "tpl") + + labels := claim.GetLabels() + if len(labels[LabelProposal]) > 63 { + t.Fatalf("proposal label length %d exceeds 63", len(labels[LabelProposal])) + } + if labels[LabelProposal] != strings.Repeat("a", 63) { + t.Errorf("proposal label = %q, want %q", labels[LabelProposal], strings.Repeat("a", 63)) + } +} + func TestBuildClaim_TemplateRef(t *testing.T) { m := NewSandboxManager(nil, "ns", "") claim := m.buildClaim("c", "p", "analysis", "my-template") From 5243949ef092457add7f251349f633d1181fd68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Reme=C5=A1?= Date: Tue, 23 Jun 2026 08:26:42 +0200 Subject: [PATCH 2/2] fix: strip trailing dots and underscores in truncateK8sName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kubernetes label values must start and end with alphanumeric characters. The previous implementation only trimmed trailing hyphens after truncation, but dots and underscores are also invalid at label value boundaries. Expand the TrimRight cutset to "-._" and add test cases. Signed-off-by: Tomáš Remeš Assisted-by: Claude Code:claude-opus-4-6 --- controller/proposal/rbac.go | 2 +- controller/proposal/rbac_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/controller/proposal/rbac.go b/controller/proposal/rbac.go index 0840374c..3ff7fa63 100644 --- a/controller/proposal/rbac.go +++ b/controller/proposal/rbac.go @@ -213,7 +213,7 @@ func rbacTargetNamespaces(proposal *agenticv1alpha1.Proposal, rbacResult *agenti func truncateK8sName(name string) string { if len(name) > 63 { - name = strings.TrimRight(name[:63], "-") + name = strings.TrimRight(name[:63], "-._") } return name } diff --git a/controller/proposal/rbac_test.go b/controller/proposal/rbac_test.go index 60aae96a..7c55daaa 100644 --- a/controller/proposal/rbac_test.go +++ b/controller/proposal/rbac_test.go @@ -477,6 +477,9 @@ func TestTruncateK8sName(t *testing.T) { {"exactly_63", strings.Repeat("a", 63), strings.Repeat("a", 63)}, {"over_63", strings.Repeat("a", 70), strings.Repeat("a", 63)}, {"trailing_dash_trimmed", strings.Repeat("a", 60) + "---" + strings.Repeat("b", 5), strings.Repeat("a", 60)}, + {"trailing_dot_trimmed", strings.Repeat("a", 60) + "..." + strings.Repeat("b", 5), strings.Repeat("a", 60)}, + {"trailing_underscore_trimmed", strings.Repeat("a", 60) + "___" + strings.Repeat("b", 5), strings.Repeat("a", 60)}, + {"trailing_mixed_trimmed", strings.Repeat("a", 58) + "-._.-" + strings.Repeat("b", 5), strings.Repeat("a", 58)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {