Skip to content

Commit 0de14ec

Browse files
Merge pull request #140732 from thockin/dv-add-prefixed-label-key
Add a DV format for k8s-prefixed-label-key Kubernetes-commit: 4e9e4144dd9c905e48bfeb7fa865b3b702cd9f41
2 parents 95258eb + 52d6a18 commit 0de14ec

4 files changed

Lines changed: 185 additions & 0 deletions

File tree

pkg/api/validate/content/kube.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,19 @@ func prefixEach(msgs []string, prefix string) []string {
9999
}
100100
return msgs
101101
}
102+
103+
// IsPrefixedLabelKey tests whether the value passed is a valid label key with
104+
// a domain prefix. This allows "example.com/key" but not "key".
105+
// If the value is not valid, a list of error strings is returned. Otherwise,
106+
// an empty list (or nil) is returned.
107+
func IsPrefixedLabelKey(value string) []string {
108+
if errs := IsLabelKey(value); len(errs) > 0 {
109+
return errs
110+
}
111+
112+
segments := strings.Split(value, "/")
113+
if len(segments) != 2 {
114+
return []string{"must include a prefix (e.g. 'example.com/key')"}
115+
}
116+
return nil
117+
}

pkg/api/validate/content/kube_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,48 @@ func TestIsLabelValue(t *testing.T) {
9898
}
9999
}
100100
}
101+
102+
func TestIsPrefixedLabelKey(t *testing.T) {
103+
successCases := []string{
104+
"simple/simple",
105+
"now-with-dashes/simple",
106+
"now-with-dashes/now-with-dashes",
107+
"now.with.dots/simple",
108+
"now-with.dashes-and.dots/simple",
109+
"1-num.2-num/3-num",
110+
"1234/5678",
111+
"1.2.3.4/5678",
112+
"example.com/Uppercase_Is_OK_123",
113+
strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63),
114+
}
115+
for i := range successCases {
116+
if errs := IsPrefixedLabelKey(successCases[i]); len(errs) != 0 {
117+
t.Errorf("case[%d]: %q: expected success: %v", i, successCases[i], errs)
118+
}
119+
}
120+
121+
errorCases := []string{
122+
"simple",
123+
"now-with-dashes",
124+
"1-starts-with-num",
125+
"1234",
126+
"Uppercase_Is_OK_123",
127+
"requests.storage-foo",
128+
strings.Repeat("a", 63),
129+
"nospecialchars%^=@",
130+
"cantendwithadash-",
131+
"-cantstartwithadash-",
132+
"only/one/slash",
133+
"Example.com/abc",
134+
"example_com/abc",
135+
"example.com/",
136+
"/simple",
137+
strings.Repeat("a", 64),
138+
strings.Repeat("a", 254) + "/abc",
139+
}
140+
for i := range errorCases {
141+
if errs := IsPrefixedLabelKey(errorCases[i]); len(errs) == 0 {
142+
t.Errorf("case[%d]: %q: expected failure", i, errorCases[i])
143+
}
144+
}
145+
}

pkg/api/validate/strfmt.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@ func LabelKey[T ~string](_ context.Context, op operation.Operation, fldPath *fie
9797
return allErrs
9898
}
9999

100+
// PrefixedLabelKey verifies that the specified value is a valid label key with
101+
// a domain prefix.
102+
// A prefixed label key is composed of a prefix and a name, separated by a '/'.
103+
// The name part is required and must:
104+
// - be 63 characters or less
105+
// - begin and end with an alphanumeric character ([a-z0-9A-Z])
106+
// - contain only alphanumeric characters, dashes (-), underscores (_), or dots (.)
107+
//
108+
// The prefix must:
109+
// - be a DNS subdomain
110+
// - be no more than 253 characters
111+
func PrefixedLabelKey[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
112+
if value == nil {
113+
return nil
114+
}
115+
var allErrs field.ErrorList
116+
for _, msg := range content.IsPrefixedLabelKey((string)(*value)) {
117+
allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-prefixed-label-key"))
118+
}
119+
return allErrs
120+
}
121+
100122
// LongNameCaseless verifies that the specified value is a valid "long name"
101123
// (sometimes known as a "DNS subdomain"), but is case-insensitive.
102124
// - must not be empty

pkg/api/validate/strfmt_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,108 @@ func TestLabelKey(t *testing.T) {
239239
}
240240
}
241241

242+
func TestPrefixedLabelKey(t *testing.T) {
243+
ctx := context.Background()
244+
fldPath := field.NewPath("test")
245+
246+
testCases := []struct {
247+
name string
248+
input string
249+
wantErrs field.ErrorList
250+
}{{
251+
name: "valid key",
252+
input: "app",
253+
wantErrs: field.ErrorList{
254+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
255+
},
256+
}, {
257+
name: "valid key with dash",
258+
input: "app-name",
259+
wantErrs: field.ErrorList{
260+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
261+
},
262+
}, {
263+
name: "valid key with dot",
264+
input: "app.name",
265+
wantErrs: field.ErrorList{
266+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
267+
},
268+
}, {
269+
name: "valid key with underscore",
270+
input: "app_name",
271+
wantErrs: field.ErrorList{
272+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
273+
},
274+
}, {
275+
name: "valid key with prefix",
276+
input: "example.com/app",
277+
wantErrs: nil,
278+
}, {
279+
name: "valid key with long prefix",
280+
input: strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "." + strings.Repeat("d", 55) + "/app",
281+
wantErrs: nil,
282+
}, {
283+
name: "invalid: empty string",
284+
input: "",
285+
wantErrs: field.ErrorList{
286+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
287+
},
288+
}, {
289+
name: "invalid: starts with dash",
290+
input: "-app",
291+
wantErrs: field.ErrorList{
292+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
293+
},
294+
}, {
295+
name: "invalid: ends with dash",
296+
input: "app-",
297+
wantErrs: field.ErrorList{
298+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
299+
},
300+
}, {
301+
name: "invalid: contains invalid characters",
302+
input: "app^",
303+
wantErrs: field.ErrorList{
304+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
305+
},
306+
}, {
307+
name: "invalid: name too long",
308+
input: strings.Repeat("a", 64),
309+
wantErrs: field.ErrorList{
310+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
311+
},
312+
}, {
313+
name: "invalid: prefix too long",
314+
input: strings.Repeat("a", 254) + "/app",
315+
wantErrs: field.ErrorList{
316+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
317+
},
318+
}, {
319+
name: "invalid: prefix is not a DNS subdomain",
320+
input: "example-.com/app",
321+
wantErrs: field.ErrorList{
322+
field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-prefixed-label-key"),
323+
},
324+
}, {
325+
name: "nil value",
326+
input: "", // This will be handled by setting value to nil in the test runner
327+
wantErrs: nil,
328+
}}
329+
330+
matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
331+
for _, tc := range testCases {
332+
t.Run(tc.name, func(t *testing.T) {
333+
var value *string
334+
if tc.name != "nil value" {
335+
v := tc.input
336+
value = &v
337+
}
338+
gotErrs := PrefixedLabelKey(ctx, operation.Operation{}, fldPath, value, nil)
339+
matcher.Test(t, tc.wantErrs, gotErrs)
340+
})
341+
}
342+
}
343+
242344
func TestK8sUUID(t *testing.T) {
243345
ctx := context.Background()
244346
fldPath := field.NewPath("test")

0 commit comments

Comments
 (0)