diff --git a/cmd/deploy.go b/cmd/deploy.go index e053004b0f..9c8a87c621 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -347,6 +347,20 @@ Valid examples: Note: Domain must be configured on your Knative cluster, or it will be ignored. +For more options, run 'func deploy --help'`, err) + } + if errors.Is(err, fn.ErrInvalidNamespace) { + return fmt.Errorf(`%w + +Invalid namespace name. Kubernetes namespaces must: + - Contain only lowercase letters, numbers, and hyphens (-) + - Start with a letter and end with a letter or number + - Be 63 characters or less + +Valid examples: + func deploy --namespace myapp + func deploy --namespace my-app-123 + For more options, run 'func deploy --help'`, err) } if errors.Is(err, fn.ErrConflictingImageAndRegistry) { @@ -826,6 +840,13 @@ func (c deployConfig) Validate(cmd *cobra.Command) (err error) { return fn.ErrInvalidDomain } } + // Validate namespace format if provided + if c.Namespace != "" { + if err = utils.ValidateNamespace(c.Namespace); err != nil { + // Wrap the validation error as fn.ErrInvalidNamespace for layer consistency + return fn.ErrInvalidNamespace + } + } // Check Image Digest was included var digest bool diff --git a/pkg/functions/errors.go b/pkg/functions/errors.go index 71f6e69956..3c3e8b8f08 100644 --- a/pkg/functions/errors.go +++ b/pkg/functions/errors.go @@ -44,6 +44,9 @@ var ( // ErrClusterNotAccessible is returned when cluster connection fails (network, auth, etc) ErrClusterNotAccessible = errors.New("cluster not accessible") + + // ErrInvalidNamespace is returned when a namespace name doesn't meet Kubernetes naming requirements + ErrInvalidNamespace = errors.New("invalid namespace") ) // ErrNotInitialized indicates that a function is uninitialized diff --git a/pkg/utils/names.go b/pkg/utils/names.go index 525d770562..40bfa04183 100644 --- a/pkg/utils/names.go +++ b/pkg/utils/names.go @@ -26,6 +26,9 @@ type ErrInvalidLabel error // ErrInvalidDomain indicates the domain name did not pass DNS subdomain validation. type ErrInvalidDomain error +// ErrInvalidNamespace indicates the namespace name did not pass Kubernetes namespace validation. +type ErrInvalidNamespace error + // ValidateFunctionName validates that the input name is a valid function name, ie. valid DNS-1035 label. // It must consist of lower case alphanumeric characters or '-' and start with an alphabetic character and end with an alphanumeric character. // (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 { return nil } + +// ValidateNamespace validates that the input name is a valid Kubernetes namespace name, ie. valid DNS-1123 label. +// It must consist of lower case alphanumeric characters or '-', +// start with an alphabetic character, and end with an alphanumeric character +// (e.g. 'my-namespace', 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?') +func ValidateNamespace(namespace string) error { + if errs := validation.IsDNS1035Label(namespace); len(errs) > 0 { + // Reuse the error message from Kubernetes validation + // Replace "a DNS-1035 label" with more user-friendly context + errMsg := strings.Replace(strings.Join(errs, ""), "a DNS-1035 label", fmt.Sprintf("Namespace '%v'", namespace), 1) + return ErrInvalidNamespace(errors.New(errMsg)) + } + return nil +} diff --git a/pkg/utils/names_test.go b/pkg/utils/names_test.go index 4ebf9fbbe9..82ef98d801 100644 --- a/pkg/utils/names_test.go +++ b/pkg/utils/names_test.go @@ -231,6 +231,7 @@ func TestValidateDomain(t *testing.T) { {"example-app.com", true}, // hyphen in domain {"a.co", true}, // short domain {"123app.example.com", true}, // label starting with number + // Invalid domains {"Example.Com", false}, // uppercase not allowed {"MY-APP.COM", false}, // uppercase not allowed @@ -294,3 +295,68 @@ func TestValidateDomainEmptyString(t *testing.T) { t.Fatal("String with only whitespace should be invalid") } } + +// TestValidateNamespace tests that only correct Kubernetes namespace names are accepted +func TestValidateNamespace(t *testing.T) { + cases := []struct { + In string + Valid bool + }{ + // Valid namespaces + {"default", true}, + {"kube-system", true}, + {"my-namespace", true}, + {"myapp", true}, + {"my-app-123", true}, + {"prod", true}, + {"test-123", true}, + {"a", true}, + {"a-b", true}, + {"abc-123-xyz", true}, + + // Invalid namespaces + {"123app", false}, // cannot start with number (K8s requirement) + {"123invalid", false}, // cannot start with number (K8s requirement) + {"1", false}, // cannot start with number (K8s requirement) + {"My-App", false}, // uppercase not allowed + {"MY-APP", false}, // uppercase not allowed + {"my_app", false}, // underscore not allowed + {"my app", false}, // spaces not allowed + {"invalid namespace", false}, // spaces not allowed + {"my@app", false}, // @ not allowed + {"invalid@namespace", false}, // @ not allowed + {"-myapp", false}, // cannot start with hyphen + {"myapp-", false}, // cannot end with hyphen + {"my..app", false}, // dots not allowed + {"my/app", false}, // slash not allowed + {"my:app", false}, // colon not allowed + {"my;app", false}, // semicolon not allowed + {"my,app", false}, // comma not allowed + {"my*app", false}, // asterisk not allowed + {"my!app", false}, // exclamation not allowed + } + + for _, c := range cases { + err := ValidateNamespace(c.In) + if err != nil && c.Valid { + t.Fatalf("Unexpected error for valid namespace: %v, namespace: '%v'", err, c.In) + } + if err == nil && !c.Valid { + t.Fatalf("Expected error for invalid namespace: '%v'", c.In) + } + } +} + +func TestValidateNamespaceErrMsg(t *testing.T) { + invalidNamespace := "my@app" + errMsgPrefix := fmt.Sprintf("Namespace '%v'", invalidNamespace) + + err := ValidateNamespace(invalidNamespace) + if err != nil { + if !strings.HasPrefix(err.Error(), errMsgPrefix) { + t.Fatalf("Unexpected error message: %v, the message should start with '%v' string", err.Error(), errMsgPrefix) + } + } else { + t.Fatalf("Expected error for invalid namespace: %v", invalidNamespace) + } +}