Skip to content

Commit 11a85d5

Browse files
committed
Add namespace validation to catch invalid names before build/deploy
Signed-off-by: RayyanSeliya <rayyanseliya786@gmail.com>
1 parent b990215 commit 11a85d5

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

cmd/deploy.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"knative.dev/func/pkg/config"
2121
fn "knative.dev/func/pkg/functions"
2222
"knative.dev/func/pkg/k8s"
23+
"knative.dev/func/pkg/utils"
2324
)
2425

2526
func NewDeployCmd(newClient ClientFactory) *cobra.Command {
@@ -280,6 +281,20 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
280281
}
281282
if err = cfg.Validate(cmd); err != nil {
282283
// Layer 2: Catch technical errors and provide CLI-specific user-friendly messages
284+
if errors.Is(err, fn.ErrInvalidNamespace) {
285+
return fmt.Errorf(`%w
286+
287+
Invalid namespace name. Kubernetes namespaces must:
288+
- Contain only lowercase letters, numbers, and hyphens (-)
289+
- Start and end with a letter or number
290+
- Be 63 characters or less
291+
292+
Valid examples:
293+
func deploy --namespace myapp
294+
func deploy --namespace my-app-123
295+
296+
For more options, run 'func deploy --help'`, err)
297+
}
283298
if errors.Is(err, fn.ErrConflictingImageAndRegistry) {
284299
return fmt.Errorf(`%w
285300
@@ -738,6 +753,14 @@ func (c deployConfig) Validate(cmd *cobra.Command) (err error) {
738753
return
739754
}
740755

756+
// Validate namespace format if provided
757+
if c.Namespace != "" {
758+
if err = utils.ValidateNamespace(c.Namespace); err != nil {
759+
// Wrap the validation error as fn.ErrInvalidNamespace for layer consistency
760+
return fn.ErrInvalidNamespace
761+
}
762+
}
763+
741764
// Check Image Digest was included
742765
var digest bool
743766
if c.Image != "" {

pkg/functions/errors.go

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

3535
// ErrConflictingImageAndRegistry is returned when both --image and --registry flags are explicitly provided
3636
ErrConflictingImageAndRegistry = errors.New("both --image and --registry flags provided")
37+
38+
// ErrInvalidNamespace is returned when a namespace name doesn't meet Kubernetes naming requirements
39+
ErrInvalidNamespace = errors.New("invalid namespace")
3740
)
3841

3942
// 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
@@ -23,6 +23,9 @@ type ErrInvalidSecretKey error
2323
// ErrInvalidLabel indicates the name did not pass label key validation, or the value did not pass label value validation.
2424
type ErrInvalidLabel error
2525

26+
// ErrInvalidNamespace indicates the namespace name did not pass Kubernetes namespace validation.
27+
type ErrInvalidNamespace error
28+
2629
// ValidateFunctionName validates that the input name is a valid function name, ie. valid DNS-1035 label.
2730
// It must consist of lower case alphanumeric characters or '-' and start with an alphabetic character and end with an alphanumeric character.
2831
// (e.g. 'my-name', or 'abc-1', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')
@@ -102,3 +105,17 @@ func ValidateLabelValue(value string) error {
102105
}
103106
return nil
104107
}
108+
109+
// ValidateNamespace validates that the input name is a valid Kubernetes namespace name, ie. valid DNS-1123 label.
110+
// It must consist of lower case alphanumeric characters or '-',
111+
// and must start and end with an alphanumeric character
112+
// (e.g. 'my-namespace', 'abc-123', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')
113+
func ValidateNamespace(namespace string) error {
114+
if errs := validation.IsDNS1123Label(namespace); len(errs) > 0 {
115+
// Reuse the error message from Kubernetes validation
116+
// Replace "a DNS-1123 label" with more user-friendly context
117+
errMsg := strings.Replace(strings.Join(errs, ""), "a DNS-1123 label", fmt.Sprintf("Namespace '%v'", namespace), 1)
118+
return ErrInvalidNamespace(errors.New(errMsg))
119+
}
120+
return nil
121+
}

pkg/utils/names_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,68 @@ func TestValidateLabelValue(t *testing.T) {
209209
}
210210
}
211211
}
212+
213+
// TestValidateNamespace tests that only correct Kubernetes namespace names are accepted
214+
func TestValidateNamespace(t *testing.T) {
215+
cases := []struct {
216+
In string
217+
Valid bool
218+
}{
219+
// Valid namespaces
220+
{"default", true},
221+
{"kube-system", true},
222+
{"my-namespace", true},
223+
{"myapp", true},
224+
{"my-app-123", true},
225+
{"prod", true},
226+
{"test-123", true},
227+
{"a", true},
228+
{"a-b", true},
229+
{"abc-123-xyz", true},
230+
231+
// Invalid namespaces
232+
{"", false}, // empty
233+
{"My-App", false}, // uppercase not allowed
234+
{"MY-APP", false}, // uppercase not allowed
235+
{"123app", false}, // cannot start with number
236+
{"123invalid", false}, // cannot start with number
237+
{"my_app", false}, // underscore not allowed
238+
{"my app", false}, // spaces not allowed
239+
{"invalid namespace", false}, // spaces not allowed
240+
{"my@app", false}, // @ not allowed
241+
{"invalid@namespace", false}, // @ not allowed
242+
{"-myapp", false}, // cannot start with hyphen
243+
{"myapp-", false}, // cannot end with hyphen
244+
{"my..app", false}, // dots not allowed
245+
{"my/app", false}, // slash not allowed
246+
{"my:app", false}, // colon not allowed
247+
{"my;app", false}, // semicolon not allowed
248+
{"my,app", false}, // comma not allowed
249+
{"my*app", false}, // asterisk not allowed
250+
{"my!app", false}, // exclamation not allowed
251+
}
252+
253+
for _, c := range cases {
254+
err := ValidateNamespace(c.In)
255+
if err != nil && c.Valid {
256+
t.Fatalf("Unexpected error for valid namespace: %v, namespace: '%v'", err, c.In)
257+
}
258+
if err == nil && !c.Valid {
259+
t.Fatalf("Expected error for invalid namespace: '%v'", c.In)
260+
}
261+
}
262+
}
263+
264+
func TestValidateNamespaceErrMsg(t *testing.T) {
265+
invalidNamespace := "123invalid"
266+
errMsgPrefix := fmt.Sprintf("Namespace '%v'", invalidNamespace)
267+
268+
err := ValidateNamespace(invalidNamespace)
269+
if err != nil {
270+
if !strings.HasPrefix(err.Error(), errMsgPrefix) {
271+
t.Fatalf("Unexpected error message: %v, the message should start with '%v' string", err.Error(), errMsgPrefix)
272+
}
273+
} else {
274+
t.Fatalf("Expected error for invalid namespace: %v", invalidNamespace)
275+
}
276+
}

0 commit comments

Comments
 (0)