Skip to content

Commit af45a47

Browse files
authored
Add namespace validation to catch invalid names before build/deploy (#3133)
* Add namespace validation to catch invalid names before build/deploy Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * fix the linter errors and refactor such that test not fail Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * fix the goimports Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * refactor based on the rfc-1123 dns ruling Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * use dns-1035 validation to enforce alphabetic start for namespace Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * fixed some inconcistencies due to merge conflicts Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> * goimports after merge conflict Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com> --------- Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com>
1 parent ef700e7 commit af45a47

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

cmd/deploy.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,20 @@ Valid examples:
347347
348348
Note: Domain must be configured on your Knative cluster, or it will be ignored.
349349
350+
For more options, run 'func deploy --help'`, err)
351+
}
352+
if errors.Is(err, fn.ErrInvalidNamespace) {
353+
return fmt.Errorf(`%w
354+
355+
Invalid namespace name. Kubernetes namespaces must:
356+
- Contain only lowercase letters, numbers, and hyphens (-)
357+
- Start with a letter and end with a letter or number
358+
- Be 63 characters or less
359+
360+
Valid examples:
361+
func deploy --namespace myapp
362+
func deploy --namespace my-app-123
363+
350364
For more options, run 'func deploy --help'`, err)
351365
}
352366
if errors.Is(err, fn.ErrConflictingImageAndRegistry) {
@@ -826,6 +840,13 @@ func (c deployConfig) Validate(cmd *cobra.Command) (err error) {
826840
return fn.ErrInvalidDomain
827841
}
828842
}
843+
// Validate namespace format if provided
844+
if c.Namespace != "" {
845+
if err = utils.ValidateNamespace(c.Namespace); err != nil {
846+
// Wrap the validation error as fn.ErrInvalidNamespace for layer consistency
847+
return fn.ErrInvalidNamespace
848+
}
849+
}
829850

830851
// Check Image Digest was included
831852
var digest bool

pkg/functions/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ var (
4444

4545
// ErrClusterNotAccessible is returned when cluster connection fails (network, auth, etc)
4646
ErrClusterNotAccessible = errors.New("cluster not accessible")
47+
48+
// ErrInvalidNamespace is returned when a namespace name doesn't meet Kubernetes naming requirements
49+
ErrInvalidNamespace = errors.New("invalid namespace")
4750
)
4851

4952
// ErrNotInitialized indicates that a function is uninitialized

pkg/utils/names.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ type ErrInvalidLabel error
2626
// ErrInvalidDomain indicates the domain name did not pass DNS subdomain validation.
2727
type ErrInvalidDomain error
2828

29+
// ErrInvalidNamespace indicates the namespace name did not pass Kubernetes namespace validation.
30+
type ErrInvalidNamespace error
31+
2932
// ValidateFunctionName validates that the input name is a valid function name, ie. valid DNS-1035 label.
3033
// It must consist of lower case alphanumeric characters or '-' and start with an alphabetic character and end with an alphanumeric character.
3134
// (e.g. 'my-name', or 'abc-1', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')
@@ -125,3 +128,17 @@ func ValidateDomain(domain string) error {
125128

126129
return nil
127130
}
131+
132+
// ValidateNamespace validates that the input name is a valid Kubernetes namespace name, ie. valid DNS-1123 label.
133+
// It must consist of lower case alphanumeric characters or '-',
134+
// start with an alphabetic character, and end with an alphanumeric character
135+
// (e.g. 'my-namespace', 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')
136+
func ValidateNamespace(namespace string) error {
137+
if errs := validation.IsDNS1035Label(namespace); len(errs) > 0 {
138+
// Reuse the error message from Kubernetes validation
139+
// Replace "a DNS-1035 label" with more user-friendly context
140+
errMsg := strings.Replace(strings.Join(errs, ""), "a DNS-1035 label", fmt.Sprintf("Namespace '%v'", namespace), 1)
141+
return ErrInvalidNamespace(errors.New(errMsg))
142+
}
143+
return nil
144+
}

pkg/utils/names_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ func TestValidateDomain(t *testing.T) {
231231
{"example-app.com", true}, // hyphen in domain
232232
{"a.co", true}, // short domain
233233
{"123app.example.com", true}, // label starting with number
234+
234235
// Invalid domains
235236
{"Example.Com", false}, // uppercase not allowed
236237
{"MY-APP.COM", false}, // uppercase not allowed
@@ -294,3 +295,68 @@ func TestValidateDomainEmptyString(t *testing.T) {
294295
t.Fatal("String with only whitespace should be invalid")
295296
}
296297
}
298+
299+
// TestValidateNamespace tests that only correct Kubernetes namespace names are accepted
300+
func TestValidateNamespace(t *testing.T) {
301+
cases := []struct {
302+
In string
303+
Valid bool
304+
}{
305+
// Valid namespaces
306+
{"default", true},
307+
{"kube-system", true},
308+
{"my-namespace", true},
309+
{"myapp", true},
310+
{"my-app-123", true},
311+
{"prod", true},
312+
{"test-123", true},
313+
{"a", true},
314+
{"a-b", true},
315+
{"abc-123-xyz", true},
316+
317+
// Invalid namespaces
318+
{"123app", false}, // cannot start with number (K8s requirement)
319+
{"123invalid", false}, // cannot start with number (K8s requirement)
320+
{"1", false}, // cannot start with number (K8s requirement)
321+
{"My-App", false}, // uppercase not allowed
322+
{"MY-APP", false}, // uppercase not allowed
323+
{"my_app", false}, // underscore not allowed
324+
{"my app", false}, // spaces not allowed
325+
{"invalid namespace", false}, // spaces not allowed
326+
{"my@app", false}, // @ not allowed
327+
{"invalid@namespace", false}, // @ not allowed
328+
{"-myapp", false}, // cannot start with hyphen
329+
{"myapp-", false}, // cannot end with hyphen
330+
{"my..app", false}, // dots not allowed
331+
{"my/app", false}, // slash not allowed
332+
{"my:app", false}, // colon not allowed
333+
{"my;app", false}, // semicolon not allowed
334+
{"my,app", false}, // comma not allowed
335+
{"my*app", false}, // asterisk not allowed
336+
{"my!app", false}, // exclamation not allowed
337+
}
338+
339+
for _, c := range cases {
340+
err := ValidateNamespace(c.In)
341+
if err != nil && c.Valid {
342+
t.Fatalf("Unexpected error for valid namespace: %v, namespace: '%v'", err, c.In)
343+
}
344+
if err == nil && !c.Valid {
345+
t.Fatalf("Expected error for invalid namespace: '%v'", c.In)
346+
}
347+
}
348+
}
349+
350+
func TestValidateNamespaceErrMsg(t *testing.T) {
351+
invalidNamespace := "my@app"
352+
errMsgPrefix := fmt.Sprintf("Namespace '%v'", invalidNamespace)
353+
354+
err := ValidateNamespace(invalidNamespace)
355+
if err != nil {
356+
if !strings.HasPrefix(err.Error(), errMsgPrefix) {
357+
t.Fatalf("Unexpected error message: %v, the message should start with '%v' string", err.Error(), errMsgPrefix)
358+
}
359+
} else {
360+
t.Fatalf("Expected error for invalid namespace: %v", invalidNamespace)
361+
}
362+
}

0 commit comments

Comments
 (0)