Skip to content

Commit 6333c3b

Browse files
committed
fixing pipeline_upload_client_kubernetes
Signed-off-by: Nelesh Singla <117123879+nsingla@users.noreply.github.com>
1 parent 2800a2e commit 6333c3b

6 files changed

Lines changed: 143 additions & 43 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2026 The Kubeflow Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common
16+
17+
import (
18+
"strings"
19+
"unicode/utf8"
20+
21+
"github.com/kubeflow/pipelines/backend/src/common/util"
22+
)
23+
24+
// MaxTagKeyLength is the maximum allowed length (in characters) for a tag key.
25+
// Consistent with Kubernetes label value length limit (63 characters).
26+
const MaxTagKeyLength = 63
27+
28+
// MaxTagValueLength is the maximum allowed length (in characters) for a tag value.
29+
// Consistent with Kubernetes label value length limit (63 characters).
30+
const MaxTagValueLength = 63
31+
32+
// MaxTagsPerEntity is the maximum number of tags allowed on a single entity.
33+
const MaxTagsPerEntity = 20
34+
35+
// ValidateTags checks that tags conform to all constraints:
36+
// - maximum number of tags per entity
37+
// - no empty keys
38+
// - keys must not contain "." (conflicts with tag filter prefix "tags.")
39+
// - key and value character lengths within limits
40+
func ValidateTags(tags map[string]string) error {
41+
if len(tags) > MaxTagsPerEntity {
42+
return util.NewInvalidInputError("number of tags (%d) exceeds maximum of %d per entity", len(tags), MaxTagsPerEntity)
43+
}
44+
for key, value := range tags {
45+
if key == "" {
46+
return util.NewInvalidInputError("tag key cannot be empty")
47+
}
48+
if strings.Contains(key, ".") {
49+
return util.NewInvalidInputError("tag key %q must not contain '.' character", key)
50+
}
51+
if utf8.RuneCountInString(key) > MaxTagKeyLength {
52+
return util.NewInvalidInputError("tag key %q exceeds maximum length of %d characters", key, MaxTagKeyLength)
53+
}
54+
if utf8.RuneCountInString(value) > MaxTagValueLength {
55+
return util.NewInvalidInputError("tag value %q for key %q exceeds maximum length of %d characters", value, key, MaxTagValueLength)
56+
}
57+
}
58+
return nil
59+
}

backend/src/apiserver/resource/resource_manager.go

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import (
2323
"net/url"
2424
"reflect"
2525
"strconv"
26-
"strings"
2726
"time"
28-
"unicode/utf8"
2927

3028
apiv2beta1 "github.com/kubeflow/pipelines/backend/api/v2beta1/go_client"
3129
scheduledworkflow "github.com/kubeflow/pipelines/backend/src/crd/pkg/apis/scheduledworkflow/v1beta1"
@@ -394,48 +392,22 @@ func (r *ResourceManager) UpdatePipelineDefaultVersion(pipelineId string, versio
394392

395393
// MaxTagKeyLength is the maximum allowed length (in characters) for a tag key.
396394
// Consistent with Kubernetes label value length limit (63 characters).
397-
const MaxTagKeyLength = 63
395+
const MaxTagKeyLength = common.MaxTagKeyLength
398396

399397
// MaxTagValueLength is the maximum allowed length (in characters) for a tag value.
400398
// Consistent with Kubernetes label value length limit (63 characters).
401-
const MaxTagValueLength = 63
399+
const MaxTagValueLength = common.MaxTagValueLength
402400

403401
// MaxTagsPerEntity is the maximum number of tags allowed on a single pipeline or pipeline version.
404-
const MaxTagsPerEntity = 20
405-
406-
// validateTags checks that tags conform to all constraints:
407-
// - maximum number of tags per entity
408-
// - no empty keys
409-
// - keys must not contain "." (conflicts with tag filter prefix "tags.")
410-
// - key and value character lengths within limits
411-
func validateTags(tags map[string]string) error {
412-
if len(tags) > MaxTagsPerEntity {
413-
return util.NewInvalidInputError("number of tags (%d) exceeds maximum of %d per entity", len(tags), MaxTagsPerEntity)
414-
}
415-
for key, value := range tags {
416-
if key == "" {
417-
return util.NewInvalidInputError("tag key cannot be empty")
418-
}
419-
if strings.Contains(key, ".") {
420-
return util.NewInvalidInputError("tag key %q must not contain '.' character", key)
421-
}
422-
if utf8.RuneCountInString(key) > MaxTagKeyLength {
423-
return util.NewInvalidInputError("tag key %q exceeds maximum length of %d characters", key, MaxTagKeyLength)
424-
}
425-
if utf8.RuneCountInString(value) > MaxTagValueLength {
426-
return util.NewInvalidInputError("tag value %q for key %q exceeds maximum length of %d characters", value, key, MaxTagValueLength)
427-
}
428-
}
429-
return nil
430-
}
402+
const MaxTagsPerEntity = common.MaxTagsPerEntity
431403

432404
// UpdatePipeline updates mutable fields of a pipeline (display_name, tags).
433405
// Both fields are updated in a single transaction via UpdatePipelineFields.
434406
func (r *ResourceManager) UpdatePipeline(pipelineID string, displayName string, tags map[string]string) (*model.Pipeline, error) {
435407
if pipelineID == "" {
436408
return nil, util.NewInvalidInputError("pipeline id cannot be empty when updating pipeline")
437409
}
438-
if err := validateTags(tags); err != nil {
410+
if err := common.ValidateTags(tags); err != nil {
439411
return nil, err
440412
}
441413
// Update fields and tags in a single transaction to prevent deadlocks.
@@ -452,7 +424,7 @@ func (r *ResourceManager) UpdatePipelineVersion(pipelineVersionID string, displa
452424
if pipelineVersionID == "" {
453425
return nil, util.NewInvalidInputError("pipeline version id cannot be empty when updating pipeline version")
454426
}
455-
if err := validateTags(tags); err != nil {
427+
if err := common.ValidateTags(tags); err != nil {
456428
return nil, err
457429
}
458430
// Update fields and tags in a single transaction to prevent deadlocks.
@@ -474,7 +446,7 @@ func (r *ResourceManager) CreatePipeline(p *model.Pipeline) (*model.Pipeline, er
474446
p.DisplayName = p.Name
475447
}
476448

477-
if err := validateTags(p.Tags); err != nil {
449+
if err := common.ValidateTags(p.Tags); err != nil {
478450
return nil, err
479451
}
480452

@@ -498,10 +470,10 @@ func (r *ResourceManager) CreatePipeline(p *model.Pipeline) (*model.Pipeline, er
498470
// Creates a pipeline and a pipeline version.
499471
// This is used when two resources need to be created in a single DB transaction.
500472
func (r *ResourceManager) CreatePipelineAndPipelineVersion(p *model.Pipeline, pv *model.PipelineVersion) (*model.Pipeline, *model.PipelineVersion, error) {
501-
if err := validateTags(p.Tags); err != nil {
473+
if err := common.ValidateTags(p.Tags); err != nil {
502474
return nil, nil, err
503475
}
504-
if err := validateTags(pv.Tags); err != nil {
476+
if err := common.ValidateTags(pv.Tags); err != nil {
505477
return nil, nil, err
506478
}
507479

@@ -1909,7 +1881,7 @@ func (r *ResourceManager) CreatePipelineVersion(pv *model.PipelineVersion) (*mod
19091881
pv.Status = model.PipelineVersionCreating
19101882
pv.PipelineSpec = model.LargeText(string(tmpl.Bytes()))
19111883

1912-
if err := validateTags(pv.Tags); err != nil {
1884+
if err := common.ValidateTags(pv.Tags); err != nil {
19131885
return nil, err
19141886
}
19151887

backend/src/apiserver/resource/resource_manager_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import (
1818
"context"
1919
"encoding/json"
2020
"fmt"
21-
"net/url"
2221
"io"
22+
"net/url"
2323
"strings"
2424
"testing"
2525
"time"
@@ -4620,7 +4620,7 @@ func TestValidateTags(t *testing.T) {
46204620
}
46214621
for _, tt := range tests {
46224622
t.Run(tt.name, func(t *testing.T) {
4623-
err := validateTags(tt.tags)
4623+
err := common.ValidateTags(tt.tags)
46244624
if tt.wantErr {
46254625
assert.NotNil(t, err)
46264626
assert.Contains(t, err.Error(), tt.errMsg)

backend/src/common/client/api_server/v2/pipeline_upload_client_kubernetes.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ func (c *PipelineUploadClientKubernetes) Upload(parameters *params.UploadPipelin
156156
fmt.Sprintf("Failed to parse pipeline tags. Params: '%v'", parameters),
157157
"Failed to upload pipeline")
158158
}
159+
if err := common.ValidateTags(tags); err != nil {
160+
return nil, util.NewUserError(err,
161+
fmt.Sprintf("Failed to validate pipeline tags. Params: '%v'", parameters),
162+
"Failed to upload pipeline")
163+
}
159164

160165
pipelineModel := apimodel.Pipeline{
161166
Name: name,
@@ -274,6 +279,11 @@ func (c *PipelineUploadClientKubernetes) UploadPipelineVersion(filePath string,
274279
fmt.Sprintf("Failed to parse pipeline version tags. Params: '%v'", parameters),
275280
"Failed to upload pipeline version")
276281
}
282+
if err := common.ValidateTags(tags); err != nil {
283+
return nil, util.NewUserError(err,
284+
fmt.Sprintf("Failed to validate pipeline version tags. Params: '%v'", parameters),
285+
"Failed to upload pipeline version")
286+
}
277287

278288
modelPipelineVersion := apimodel.PipelineVersion{
279289
Name: name,

backend/src/common/client/api_server/v2/pipeline_upload_client_kubernetes_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,45 @@ func TestUploadPipelineVersionKubernetesClient_InvalidTagsReturnsError(t *testin
144144
}
145145
}
146146

147+
func TestUploadPipelineVersionKubernetesClient_InvalidTagValueLengthReturnsError(t *testing.T) {
148+
pipelineID := "pipeline-id"
149+
namespace := "kubeflow"
150+
pipeline := &k8sapi.Pipeline{
151+
ObjectMeta: metav1.ObjectMeta{
152+
Name: "test-pipeline",
153+
Namespace: namespace,
154+
UID: types.UID(pipelineID),
155+
},
156+
}
157+
158+
fakeClient := fake.NewClientBuilder().
159+
WithScheme(scheme).
160+
WithObjects(pipeline).
161+
Build()
162+
client := &PipelineUploadClientKubernetes{ctrlClient: fakeClient, namespace: namespace}
163+
164+
specPath := writePipelineSpecFile(t)
165+
rawTags := `{"team":"this-value-is-way-too-long-for-tags-and-exceeds-the-sixty-three-char-limit"}`
166+
versionName := "tagged-version"
167+
params := params.NewUploadPipelineVersionParams()
168+
params.Pipelineid = &pipelineID
169+
params.SetName(&versionName)
170+
params.SetTags(&rawTags)
171+
172+
_, err := client.UploadPipelineVersion(specPath, params)
173+
if err == nil {
174+
t.Fatalf("expected UploadPipelineVersion() to fail for tag value longer than 63 characters")
175+
}
176+
177+
var versions k8sapi.PipelineVersionList
178+
if err := fakeClient.List(context.Background(), &versions, &ctrlclient.ListOptions{Namespace: namespace}); err != nil {
179+
t.Fatalf("failed to list pipeline versions: %v", err)
180+
}
181+
if len(versions.Items) != 0 {
182+
t.Fatalf("expected no pipeline versions on invalid tag value length, got %d", len(versions.Items))
183+
}
184+
}
185+
147186
func writePipelineSpecFile(t *testing.T) string {
148187
t.Helper()
149188

backend/test/v2/api/pipeline_upload_api_test.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ var _ = Describe("Verify Pipeline Upload with Tags >", Label(constants.POSITIVE,
112112
tagsJSON, err := testutil.TagsMapToJSONStringPtr(emptyTags)
113113
Expect(err).NotTo(HaveOccurred())
114114
testContext.Pipeline.UploadParams.Tags = tagsJSON
115-
uploadPipelineAndVerify(pipelineSpecFilePath, &testContext.Pipeline.PipelineGeneratedName, nil)
115+
createdPipeline := uploadPipelineAndVerify(pipelineSpecFilePath, &testContext.Pipeline.PipelineGeneratedName, nil)
116+
Expect(createdPipeline.Tags).To(BeEmpty(), "Pipeline uploaded with empty tags should have empty tags")
116117
})
117118

118119
It(fmt.Sprintf("Upload %s pipeline without tags and verify no tags are returned", helloWorldPipelineFileName), func() {
@@ -306,7 +307,11 @@ var _ = Describe("Verify Pipeline Upload Version with Tags Failure >", Label("Ne
306307
Expect(err).NotTo(HaveOccurred())
307308
parameters.Tags = tagsJSON
308309

309-
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, "Failed to upload pipeline version")
310+
expectedErrMsg := "Failed to upload pipeline version"
311+
if *config.UploadPipelinesWithKubernetes {
312+
expectedErrMsg = "Failed to validate pipeline version tags"
313+
}
314+
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, expectedErrMsg)
310315
})
311316

312317
It("Upload a pipeline version with tag value exceeding 63 characters", func() {
@@ -323,7 +328,11 @@ var _ = Describe("Verify Pipeline Upload Version with Tags Failure >", Label("Ne
323328
Expect(err).NotTo(HaveOccurred())
324329
parameters.Tags = tagsJSON
325330

326-
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, "Failed to upload pipeline version")
331+
expectedErrMsg := "Failed to upload pipeline version"
332+
if *config.UploadPipelinesWithKubernetes {
333+
expectedErrMsg = "Failed to validate pipeline version tags"
334+
}
335+
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, expectedErrMsg)
327336
})
328337

329338
It("Upload a pipeline version with invalid tags JSON format", func() {
@@ -337,7 +346,11 @@ var _ = Describe("Verify Pipeline Upload Version with Tags Failure >", Label("Ne
337346
invalidJSON := "not-valid-json"
338347
parameters.SetTags(&invalidJSON)
339348

340-
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, "Failed to upload pipeline version")
349+
expectedErrMsg := "Failed to upload pipeline version"
350+
if *config.UploadPipelinesWithKubernetes {
351+
expectedErrMsg = "Failed to parse pipeline version tags"
352+
}
353+
uploadPipelineVersionAndVerifyFailure(pipelineSpecFilePath, parameters, expectedErrMsg)
341354
})
342355
})
343356
})
@@ -465,6 +478,13 @@ func uploadPipelineAndVerify(pipelineFilePath string, pipelineName *string, pipe
465478
// Normalize CreatedAt to UTC to avoid timezone location mismatch in deep comparison
466479
createdPipelineFromDB.CreatedAt = strfmt.DateTime(time.Time(createdPipelineFromDB.CreatedAt).UTC())
467480
createdPipeline.CreatedAt = strfmt.DateTime(time.Time(createdPipeline.CreatedAt).UTC())
481+
// Normalize empty tag maps to nil for stable comparisons across storage backends.
482+
if len(createdPipelineFromDB.Tags) == 0 {
483+
createdPipelineFromDB.Tags = nil
484+
}
485+
if len(createdPipeline.Tags) == 0 {
486+
createdPipeline.Tags = nil
487+
}
468488
Expect(createdPipelineFromDB).To(Equal(*createdPipeline))
469489
matcher.MatchPipelines(&createdPipelineFromDB, testContext.Pipeline.ExpectedPipeline)
470490

0 commit comments

Comments
 (0)