Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/functions/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions pkg/utils/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])?')
Expand Down Expand Up @@ -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])?')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like it if there could be a link in the comment to the k8s docs so the source is easily accessible for what the limits here are.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matejvasek @lkingland anything you can think of we are missing here for the namespace rules? or any other labels?

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
}
66 changes: 66 additions & 0 deletions pkg/utils/names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Loading