Skip to content
Draft
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
24 changes: 24 additions & 0 deletions internal/sandbox/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sandbox
import (
"fmt"
"math/rand"
"regexp"
)

var colors = []string{
Expand All @@ -19,6 +20,29 @@ var cities = []string{
"prague", "lisbon", "havana", "bogota", "nairobi",
}

// MaxNameLength is the maximum allowed length for a sandbox name (DNS label limit).
const MaxNameLength = 63

// namePattern matches lowercase alphanumeric strings with optional hyphens
// between segments. Leading/trailing/consecutive hyphens are not allowed.
var namePattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)

// ValidateName checks that name contains only lowercase letters, numbers, and
// hyphens, with no leading/trailing/consecutive hyphens, and is at most 63
// characters long (DNS label compatible).
func ValidateName(name string) error {
if name == "" {
return fmt.Errorf("sandbox name must not be empty")
}
if len(name) > MaxNameLength {
return fmt.Errorf("sandbox name %q exceeds maximum length of %d characters", name, MaxNameLength)
}
if !namePattern.MatchString(name) {
return fmt.Errorf("sandbox name %q is invalid: must contain only lowercase letters, numbers, and hyphens (no leading, trailing, or consecutive hyphens)", name)
}
return nil
}

// GenerateName returns a random name in the format "{color}-{city}".
func GenerateName() string {
color := colors[rand.Intn(len(colors))]
Expand Down
51 changes: 51 additions & 0 deletions internal/sandbox/names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package sandbox

import (
"strings"
"testing"
)

func TestValidateName(t *testing.T) {
valid := []string{
"foo",
"a",
"red-tokyo",
"my-sandbox-1",
"abc123",
"a-b-c",
}
for _, name := range valid {
if err := ValidateName(name); err != nil {
t.Errorf("ValidateName(%q) = %v; want nil", name, err)
}
}

invalid := []struct {
name string
desc string
}{
{"", "empty"},
{"My-Sandbox", "uppercase"},
{"foo_bar", "underscore"},
{"-foo", "leading hyphen"},
{"foo-", "trailing hyphen"},
{"foo--bar", "consecutive hyphens"},
{"foo.bar", "dot"},
{"hello world", "space"},
{strings.Repeat("a", 64), "too long"},
}
for _, tc := range invalid {
if err := ValidateName(tc.name); err == nil {
t.Errorf("ValidateName(%q) [%s] = nil; want error", tc.name, tc.desc)
}
}
}

func TestGenerateNameIsValid(t *testing.T) {
for i := 0; i < 100; i++ {
name := GenerateName()
if err := ValidateName(name); err != nil {
t.Fatalf("GenerateName() = %q; ValidateName returned %v", name, err)
}
}
}
2 changes: 2 additions & 0 deletions pkg/amika/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func (s *serviceImpl) CreateSandbox(_ context.Context, req CreateSandboxRequest)
break
}
}
} else if err := sandbox.ValidateName(name); err != nil {
return Sandbox{}, fmt.Errorf("%w: %v", ErrInvalidArgument, err)
} else if _, err := s.sandboxes.Get(name); err == nil {
return Sandbox{}, fmt.Errorf("%w: sandbox %q already exists", ErrInvalidArgument, name)
}
Expand Down
Loading