Minions should have common logic with create

TODO: disable / document generate names for cluster scoped resources.
This commit is contained in:
Clayton Coleman 2015-01-28 21:56:57 -05:00
parent 0a87f0332b
commit e485dc93ca
5 changed files with 112 additions and 12 deletions

View File

@ -31,6 +31,8 @@ type RESTCreateStrategy interface {
// The NameGenerator will be invoked prior to validation. // The NameGenerator will be invoked prior to validation.
api.NameGenerator api.NameGenerator
// NamespaceScoped returns true if the object must be within a namespace.
NamespaceScoped() bool
// ResetBeforeCreate is invoked on create before validation to remove any fields // ResetBeforeCreate is invoked on create before validation to remove any fields
// that may not be persisted. // that may not be persisted.
ResetBeforeCreate(obj runtime.Object) ResetBeforeCreate(obj runtime.Object)
@ -52,9 +54,13 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx api.Context, obj runtime.Obje
return errors.NewInternalError(err) return errors.NewInternalError(err)
} }
if strategy.NamespaceScoped() {
if !api.ValidNamespace(ctx, objectMeta) { if !api.ValidNamespace(ctx, objectMeta) {
return errors.NewBadRequest("the namespace of the provided object does not match the namespace sent on the request") return errors.NewBadRequest("the namespace of the provided object does not match the namespace sent on the request")
} }
} else {
objectMeta.Namespace = api.NamespaceNone
}
strategy.ResetBeforeCreate(obj) strategy.ResetBeforeCreate(obj)
api.FillObjectMetaSystemFields(ctx, objectMeta) api.FillObjectMetaSystemFields(ctx, objectMeta)
api.GenerateName(strategy, objectMeta) api.GenerateName(strategy, objectMeta)

View File

@ -30,6 +30,7 @@ import (
type Tester struct { type Tester struct {
*testing.T *testing.T
storage apiserver.RESTStorage storage apiserver.RESTStorage
clusterScope bool
} }
func New(t *testing.T, storage apiserver.RESTStorage) *Tester { func New(t *testing.T, storage apiserver.RESTStorage) *Tester {
@ -39,6 +40,11 @@ func New(t *testing.T, storage apiserver.RESTStorage) *Tester {
} }
} }
func (t *Tester) ClusterScope() *Tester {
t.clusterScope = true
return t
}
func copyOrDie(obj runtime.Object) runtime.Object { func copyOrDie(obj runtime.Object) runtime.Object {
out, err := api.Scheme.Copy(obj) out, err := api.Scheme.Copy(obj)
if err != nil { if err != nil {
@ -50,7 +56,11 @@ func copyOrDie(obj runtime.Object) runtime.Object {
func (t *Tester) TestCreate(valid runtime.Object, invalid ...runtime.Object) { func (t *Tester) TestCreate(valid runtime.Object, invalid ...runtime.Object) {
t.TestCreateHasMetadata(copyOrDie(valid)) t.TestCreateHasMetadata(copyOrDie(valid))
t.TestCreateGeneratesName(copyOrDie(valid)) t.TestCreateGeneratesName(copyOrDie(valid))
if t.clusterScope {
t.TestCreateRejectsNamespace(copyOrDie(valid))
} else {
t.TestCreateRejectsMismatchedNamespace(copyOrDie(valid)) t.TestCreateRejectsMismatchedNamespace(copyOrDie(valid))
}
t.TestCreateInvokesValidation(invalid...) t.TestCreateInvokesValidation(invalid...)
} }
@ -84,8 +94,13 @@ func (t *Tester) TestCreateHasMetadata(valid runtime.Object) {
objectMeta.Name = "test" objectMeta.Name = "test"
objectMeta.Namespace = api.NamespaceDefault objectMeta.Namespace = api.NamespaceDefault
context := api.NewDefaultContext()
if t.clusterScope {
objectMeta.Namespace = api.NamespaceNone
context = api.NewContext()
}
channel, err := t.storage.(apiserver.RESTCreater).Create(api.NewDefaultContext(), valid) channel, err := t.storage.(apiserver.RESTCreater).Create(context, valid)
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
@ -139,3 +154,19 @@ func (t *Tester) TestCreateRejectsMismatchedNamespace(valid runtime.Object) {
t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error())
} }
} }
func (t *Tester) TestCreateRejectsNamespace(valid runtime.Object) {
objectMeta, err := api.ObjectMetaFor(valid)
if err != nil {
t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, valid)
}
objectMeta.Namespace = "not-default"
_, err = t.storage.(apiserver.RESTCreater).Create(api.NewDefaultContext(), valid)
if err == nil {
t.Errorf("Expected an error, but we didn't get one")
} else if strings.Contains(err.Error(), "Controller.Namespace does not match the provided context") {
t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error())
}
}

View File

@ -34,6 +34,11 @@ type rcStrategy struct {
// objects. // objects.
var ReplicationControllers RESTCreateStrategy = rcStrategy{api.Scheme, api.SimpleNameGenerator} var ReplicationControllers RESTCreateStrategy = rcStrategy{api.Scheme, api.SimpleNameGenerator}
// NamespaceScoped is true for replication controllers.
func (rcStrategy) NamespaceScoped() bool {
return true
}
// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. // ResetBeforeCreate clears fields that are not allowed to be set by end users on creation.
func (rcStrategy) ResetBeforeCreate(obj runtime.Object) { func (rcStrategy) ResetBeforeCreate(obj runtime.Object) {
controller := obj.(*api.ReplicationController) controller := obj.(*api.ReplicationController)
@ -57,6 +62,11 @@ type podStrategy struct {
// objects. // objects.
var Pods RESTCreateStrategy = podStrategy{api.Scheme, api.SimpleNameGenerator} var Pods RESTCreateStrategy = podStrategy{api.Scheme, api.SimpleNameGenerator}
// NamespaceScoped is true for pods.
func (podStrategy) NamespaceScoped() bool {
return true
}
// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. // ResetBeforeCreate clears fields that are not allowed to be set by end users on creation.
func (podStrategy) ResetBeforeCreate(obj runtime.Object) { func (podStrategy) ResetBeforeCreate(obj runtime.Object) {
pod := obj.(*api.Pod) pod := obj.(*api.Pod)
@ -80,6 +90,11 @@ type svcStrategy struct {
// objects. // objects.
var Services RESTCreateStrategy = svcStrategy{api.Scheme, api.SimpleNameGenerator} var Services RESTCreateStrategy = svcStrategy{api.Scheme, api.SimpleNameGenerator}
// NamespaceScoped is true for services.
func (svcStrategy) NamespaceScoped() bool {
return true
}
// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. // ResetBeforeCreate clears fields that are not allowed to be set by end users on creation.
func (svcStrategy) ResetBeforeCreate(obj runtime.Object) { func (svcStrategy) ResetBeforeCreate(obj runtime.Object) {
service := obj.(*api.Service) service := obj.(*api.Service)
@ -88,8 +103,36 @@ func (svcStrategy) ResetBeforeCreate(obj runtime.Object) {
service.Status = api.ServiceStatus{} service.Status = api.ServiceStatus{}
} }
// Validate validates a new pod. // Validate validates a new service.
func (svcStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { func (svcStrategy) Validate(obj runtime.Object) errors.ValidationErrorList {
service := obj.(*api.Service) service := obj.(*api.Service)
return validation.ValidateService(service) return validation.ValidateService(service)
} }
// nodeStrategy implements behavior for nodes
// TODO: move to a node specific package.
type nodeStrategy struct {
runtime.ObjectTyper
api.NameGenerator
}
// Nodes is the default logic that applies when creating and updating Node
// objects.
var Nodes RESTCreateStrategy = nodeStrategy{api.Scheme, api.SimpleNameGenerator}
// NamespaceScoped is false for services.
func (nodeStrategy) NamespaceScoped() bool {
return false
}
// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation.
func (nodeStrategy) ResetBeforeCreate(obj runtime.Object) {
_ = obj.(*api.Node)
// Nodes allow *all* fields, including status, to be set.
}
// Validate validates a new node.
func (nodeStrategy) Validate(obj runtime.Object) errors.ValidationErrorList {
node := obj.(*api.Node)
return validation.ValidateMinion(node)
}

View File

@ -24,6 +24,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
@ -54,12 +55,10 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan apiserver.RE
return nil, fmt.Errorf("not a minion: %#v", obj) return nil, fmt.Errorf("not a minion: %#v", obj)
} }
if errs := validation.ValidateMinion(minion); len(errs) > 0 { if err := rest.BeforeCreate(rest.Nodes, ctx, obj); err != nil {
return nil, kerrors.NewInvalid("minion", minion.Name, errs) return nil, err
} }
api.FillObjectMetaSystemFields(ctx, &minion.ObjectMeta)
return apiserver.MakeAsync(func() (runtime.Object, error) { return apiserver.MakeAsync(func() (runtime.Object, error) {
err := rs.registry.CreateMinion(ctx, minion) err := rs.registry.CreateMinion(ctx, minion)
if err != nil { if err != nil {

View File

@ -21,6 +21,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
) )
@ -107,11 +108,14 @@ func TestMinionRegistryValidUpdate(t *testing.T) {
} }
} }
var (
validSelector = map[string]string{"a": "b"}
invalidSelector = map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
)
func TestMinionRegistryValidatesCreate(t *testing.T) { func TestMinionRegistryValidatesCreate(t *testing.T) {
storage := NewREST(registrytest.NewMinionRegistry([]string{"foo", "bar"}, api.NodeResources{})) storage := NewREST(registrytest.NewMinionRegistry([]string{"foo", "bar"}, api.NodeResources{}))
ctx := api.NewContext() ctx := api.NewContext()
validSelector := map[string]string{"a": "b"}
invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
failureCases := map[string]api.Node{ failureCases := map[string]api.Node{
"zero-length Name": { "zero-length Name": {
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
@ -148,3 +152,20 @@ func contains(nodes *api.NodeList, nodeID string) bool {
} }
return false return false
} }
func TestCreate(t *testing.T) {
test := resttest.New(t, NewREST(registrytest.NewMinionRegistry([]string{"foo", "bar"}, api.NodeResources{}))).ClusterScope()
test.TestCreate(
// valid
&api.Node{
Status: api.NodeStatus{
HostIP: "something",
},
},
// invalid
&api.Node{
ObjectMeta: api.ObjectMeta{
Labels: invalidSelector,
},
})
}