mirror of
https://github.com/psviderski/uncloud.git
synced 2026-10-07 05:39:01 +00:00
feat(machine): add validation for machine names to accept only DNS labels
This commit is contained in:
1 parent
11e1200fe8
commit
ce1c6bd05a
12 files changed
+286
-27
No files matched your search
@@ -1,12 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
)
|
||||
|
||||
// ValidateMachineName checks that a machine name is a lowercase DNS label and doesn't conflict with
|
||||
// internal DNS query modes or machine IDs.
|
||||
func ValidateMachineName(name string) error {
|
||||
if !DNSLabelRegex.MatchString(name) {
|
||||
return fmt.Errorf("invalid machine name %q: must be 1-63 characters, lowercase letters, numbers, "+
|
||||
"and hyphens only, starting and ending with a letter or number", name)
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "rr", "nearest":
|
||||
return fmt.Errorf("invalid machine name %q: reserved for internal DNS query modes", name)
|
||||
}
|
||||
if IDRegex.MatchString(name) {
|
||||
return fmt.Errorf(
|
||||
"invalid machine name %q: must not match the machine ID format (32 hexadecimal characters)", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MachineFilter defines criteria to filter machines in ListMachines.
|
||||
type MachineFilter struct {
|
||||
// Available filters machines that are not DOWN.
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
@@ -10,6 +11,52 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateMachineName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "single letter", input: "a"},
|
||||
{name: "single digit", input: "1"},
|
||||
{name: "two characters", input: "a1"},
|
||||
{name: "generated name", input: "machine-ab12"},
|
||||
{name: "hyphens and digits", input: "1-worker-2"},
|
||||
{name: "maximum length", input: strings.Repeat("a", 63)},
|
||||
{name: "maximum length with hyphens", input: "a" + strings.Repeat("-", 61) + "1"},
|
||||
{name: "machine namespace label", input: "m"},
|
||||
{name: "mode prefix", input: "nearest-worker"},
|
||||
{name: "short hexadecimal name", input: strings.Repeat("a", 31)},
|
||||
{name: "long hexadecimal name", input: strings.Repeat("a", 33)},
|
||||
{name: "non-hexadecimal 32 characters", input: strings.Repeat("g", 32)},
|
||||
{name: "empty", wantErr: "must be 1-63 characters"},
|
||||
{name: "too long", input: strings.Repeat("a", 64), wantErr: "must be 1-63 characters"},
|
||||
{name: "uppercase", input: "VPS1", wantErr: "lowercase letters"},
|
||||
{name: "leading hyphen", input: "-worker", wantErr: "starting and ending"},
|
||||
{name: "trailing hyphen", input: "worker-", wantErr: "starting and ending"},
|
||||
{name: "underscore", input: "worker_1", wantErr: "hyphens only"},
|
||||
{name: "dot", input: "worker.example", wantErr: "hyphens only"},
|
||||
{name: "space", input: "worker 1", wantErr: "hyphens only"},
|
||||
{name: "leading whitespace", input: " worker", wantErr: "hyphens only"},
|
||||
{name: "trailing whitespace", input: "worker\t", wantErr: "hyphens only"},
|
||||
{name: "non-ASCII", input: "wörker", wantErr: "lowercase letters"},
|
||||
{name: "round-robin mode", input: "rr", wantErr: "reserved for internal DNS query modes"},
|
||||
{name: "nearest mode", input: "nearest", wantErr: "reserved for internal DNS query modes"},
|
||||
{name: "machine ID", input: "c337f00600de51ef4375c9a9a267dba5", wantErr: "machine ID format"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := ValidateMachineName(tt.input)
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineMembersList_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+23
-9
@@ -41,12 +41,30 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||
dnsLabelRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
|
||||
IDRegex = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||
DNSLabelRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$`)
|
||||
)
|
||||
|
||||
func ValidateServiceID(id string) bool {
|
||||
return serviceIDRegexp.MatchString(id)
|
||||
return IDRegex.MatchString(id)
|
||||
}
|
||||
|
||||
// ValidateServiceName checks that a service name is a lowercase DNS label and doesn't conflict with
|
||||
// the machine DNS namespace or service IDs.
|
||||
func ValidateServiceName(name string) error {
|
||||
if !DNSLabelRegex.MatchString(name) {
|
||||
return fmt.Errorf("invalid service name %q: must be 1-63 characters, lowercase letters, numbers, "+
|
||||
"and hyphens only, starting and ending with a letter or number", name)
|
||||
}
|
||||
if name == "m" {
|
||||
return fmt.Errorf("invalid service name %q: reserved for the machine DNS namespace", name)
|
||||
}
|
||||
if IDRegex.MatchString(name) {
|
||||
return fmt.Errorf(
|
||||
"invalid service name %q: must not match the service ID format (32 hexadecimal characters)", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceSpec defines the desired state of a service.
|
||||
@@ -147,12 +165,8 @@ func (s *ServiceSpec) Validate() error {
|
||||
}
|
||||
|
||||
if s.Name != "" {
|
||||
if len(s.Name) > 63 {
|
||||
return fmt.Errorf("service name too long (max 63 characters): %q", s.Name)
|
||||
}
|
||||
if !dnsLabelRegexp.MatchString(s.Name) {
|
||||
return fmt.Errorf("invalid service name: %q. must be 1-63 characters, lowercase letters, numbers, "+
|
||||
"and dashes only; must start and end with a letter or number", s.Name)
|
||||
if err := ValidateServiceName(s.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -9,6 +10,59 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceSpec_Validate_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "empty allows generated name"},
|
||||
{name: "single character", input: "a"},
|
||||
{name: "single digit", input: "1"},
|
||||
{name: "two characters", input: "a1"},
|
||||
{name: "hyphens and digits", input: "1-web-2"},
|
||||
{name: "consecutive hyphens", input: "web--1"},
|
||||
{name: "maximum length", input: strings.Repeat("a", 63)},
|
||||
{name: "maximum length with hyphens", input: "a" + strings.Repeat("-", 61) + "1"},
|
||||
{name: "round-robin mode is a valid service name", input: "rr"},
|
||||
{name: "nearest mode is a valid service name", input: "nearest"},
|
||||
{name: "machine namespace prefix", input: "m-service"},
|
||||
{name: "short hexadecimal name", input: strings.Repeat("a", 31)},
|
||||
{name: "long hexadecimal name", input: strings.Repeat("a", 33)},
|
||||
{name: "non-hexadecimal 32 characters", input: strings.Repeat("g", 32)},
|
||||
{name: "too long", input: strings.Repeat("a", 64), wantErr: "must be 1-63 characters"},
|
||||
{name: "uppercase", input: "WEB1", wantErr: "lowercase letters"},
|
||||
{name: "leading hyphen", input: "-web", wantErr: "starting and ending"},
|
||||
{name: "trailing hyphen", input: "web-", wantErr: "starting and ending"},
|
||||
{name: "hyphen only", input: "-", wantErr: "starting and ending"},
|
||||
{name: "underscore", input: "web_1", wantErr: "hyphens only"},
|
||||
{name: "dot", input: "web.example", wantErr: "hyphens only"},
|
||||
{name: "slash", input: "web/api", wantErr: "hyphens only"},
|
||||
{name: "space", input: "web 1", wantErr: "hyphens only"},
|
||||
{name: "leading whitespace", input: " web", wantErr: "hyphens only"},
|
||||
{name: "trailing whitespace", input: "web ", wantErr: "hyphens only"},
|
||||
{name: "tab", input: "web\t1", wantErr: "hyphens only"},
|
||||
{name: "newline", input: "web\n", wantErr: "hyphens only"},
|
||||
{name: "non-ASCII", input: "wéb", wantErr: "lowercase letters"},
|
||||
{name: "machine DNS namespace", input: "m", wantErr: "reserved for the machine DNS namespace"},
|
||||
{name: "service ID", input: "c337f00600de51ef4375c9a9a267dba5", wantErr: "service ID format"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
spec := ServiceSpec{Name: tt.input, Container: ContainerSpec{Image: "nginx:latest"}}
|
||||
err := spec.Validate()
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user