diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index 5dff4d7c02b..78db3b8d379 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/davecgh/go-spew/spew" ) @@ -60,3 +61,9 @@ var Semantic = conversion.EqualitiesOrDie( return a.Amount.Cmp(b.Amount) == 0 }, ) + +var standardResources = util.NewStringSet(string(ResourceMemory), string(ResourceCPU)) + +func IsStandardResourceName(str string) bool { + return standardResources.Has(str) +} diff --git a/pkg/api/helpers_test.go b/pkg/api/helpers_test.go index c8cde624c31..4ff6dba37d2 100644 --- a/pkg/api/helpers_test.go +++ b/pkg/api/helpers_test.go @@ -67,3 +67,21 @@ func TestSemantic(t *testing.T) { } } } + +func TestIsStandardResource(t *testing.T) { + testCases := []struct { + input string + output bool + }{ + {"cpu", true}, + {"memory", true}, + {"disk", false}, + {"blah", false}, + {"x.y.z", false}, + } + for i, tc := range testCases { + if IsStandardResourceName(tc.input) != tc.output { + t.Errorf("case[%d], expected: %t, got: %t", i, tc.output, !tc.output) + } + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 80b41de867b..a800631a8cd 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -764,6 +764,8 @@ type NodeResources struct { type ResourceName string const ( + // The default compute resource namespace for all standard resource types. + DefaultResourceNamespace = "kubernetes.io" // CPU, in cores. (500m = .5 cores) ResourceCPU ResourceName = "cpu" // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index a02b1145a4f..2947e2b1642 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -785,6 +785,8 @@ type NodeCondition struct { type ResourceName string const ( + // The default compute resource namespace for all standard resource types. + DefaultResourceNamespace = "kubernetes.io" // CPU, in cores. (500m = .5 cores) ResourceCPU ResourceName = "cpu" // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index b3b0f01bb3c..01d0a734c75 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -622,3 +622,30 @@ func ValidateMinionUpdate(oldMinion *api.Node, minion *api.Node) errs.Validation } return allErrs } + +// Typename is a generic representation for all compute resource typenames. +// Refer to docs/resources.md for more details. +func ValidateResourceName(str string) errs.ValidationErrorList { + if !util.IsQualifiedName(str) { + return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename format %q", str)} + } + + parts := strings.Split(str, "/") + switch len(parts) { + case 1: + if !api.IsStandardResourceName(parts[0]) { + return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename. %q is neither a standard resource type nor is fully qualified", str)} + } + break + case 2: + if parts[0] == api.DefaultResourceNamespace { + if !api.IsStandardResourceName(parts[1]) { + return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename. %q contains a compute resource type not supported", str)} + + } + } + break + } + + return errs.ValidationErrorList{} +} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 1e0975ab263..5d1bd72a8aa 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1440,3 +1440,40 @@ func TestValidateMinionUpdate(t *testing.T) { } } } + +func TestValidateResourceNames(t *testing.T) { + longString := "a" + for i := 0; i < 6; i++ { + longString += longString + } + table := []struct { + input string + success bool + }{ + {"memory", true}, + {"cpu", true}, + {"network", false}, + {"disk", false}, + {"", false}, + {".", false}, + {"..", false}, + {"kubernetes.io/cpu", true}, + {"kubernetes.io/disk", false}, + {"my.favorite.app.co/12345", true}, + {"my.favorite.app.co/_12345", false}, + {"my.favorite.app.co/12345_", false}, + {"kubernetes.io/..", false}, + {"kubernetes.io/" + longString, false}, + {"kubernetes.io//", false}, + {"kubernetes.io", false}, + {"kubernetes.io/will/not/work/", false}, + } + for _, item := range table { + err := ValidateResourceName(item.input) + if len(err) != 0 && item.success { + t.Errorf("expected no failure for input %q", item.input) + } else if len(err) == 0 && !item.success { + t.Errorf("expected failure for input %q", item.input) + } + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go index a84356888ad..a18dc874db2 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -20,10 +20,12 @@ import ( "encoding/json" "fmt" "io/ioutil" + "path" "reflect" "regexp" "runtime" "strconv" + "strings" "time" "github.com/golang/glog" @@ -183,3 +185,20 @@ func AllPtrFieldsNil(obj interface{}) bool { } return true } + +// Splits a fully qualified name and returns its namespace and name. +// Assumes that the input 'str' has been validated. +func SplitQualifiedName(str string) (string, string) { + parts := strings.Split(str, "/") + if len(parts) < 2 { + return "", str + } + + return parts[0], parts[1] +} + +// Joins 'namespace' and 'name' and returns a fully qualified name +// Assumes that the input is valid. +func JoinQualifiedName(namespace, name string) string { + return path.Join(namespace, name) +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 53eaf5f8fb3..73790c733ae 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -263,3 +263,37 @@ func TestAllPtrFieldsNil(t *testing.T) { } } } + +func TestSplitQualifiedName(t *testing.T) { + testCases := []struct { + input string + output []string + }{ + {"kubernetes.io/blah", []string{"kubernetes.io", "blah"}}, + {"blah", []string{"", "blah"}}, + {"kubernetes.io/blah/blah", []string{"kubernetes.io", "blah"}}, + } + for i, tc := range testCases { + namespace, name := SplitQualifiedName(tc.input) + if namespace != tc.output[0] || name != tc.output[1] { + t.Errorf("case[%d]: expected (%q, %q), got (%q, %q)", i, tc.output[0], tc.output[1], namespace, name) + } + } +} + +func TestJoinQualifiedName(t *testing.T) { + testCases := []struct { + input []string + output string + }{ + {[]string{"kubernetes.io", "blah"}, "kubernetes.io/blah"}, + {[]string{"blah", ""}, "blah"}, + {[]string{"kubernetes.io", "blah"}, "kubernetes.io/blah"}, + } + for i, tc := range testCases { + res := JoinQualifiedName(tc.input[0], tc.input[1]) + if res != tc.output { + t.Errorf("case[%d]: expected %q, got %q", i, tc.output, res) + } + } +}