diff --git a/pkg/api/context.go b/pkg/api/context.go index e5f4e240d5f..c3f33ba85d3 100644 --- a/pkg/api/context.go +++ b/pkg/api/context.go @@ -17,6 +17,8 @@ limitations under the License. package api import ( + stderrs "errors" + "code.google.com/p/go.net/context" ) @@ -25,7 +27,47 @@ type Context interface { Value(key interface{}) interface{} } -// NewContext instantiates a base context object for request flows +// The key type is unexported to prevent collisions +type key int + +// namespaceKey is the context key for the request namespace. +const namespaceKey key = 0 + +// NewContext instantiates a base context object for request flows. func NewContext() Context { return context.TODO() } + +// NewDefaultContext instantiates a base context object for request flows in the default namespace +func NewDefaultContext() Context { + return WithNamespace(NewContext(), NamespaceDefault) +} + +// WithValue returns a copy of parent in which the value associated with key is val. +func WithValue(parent Context, key interface{}, val interface{}) Context { + internalCtx, ok := parent.(context.Context) + if !ok { + panic(stderrs.New("Invalid context type")) + } + return context.WithValue(internalCtx, key, val) +} + +// WithNamespace returns a copy of parent in which the namespace value is set +func WithNamespace(parent Context, namespace string) Context { + return WithValue(parent, namespaceKey, namespace) +} + +// NamespaceFrom returns the value of the namespace key on the ctx +func NamespaceFrom(ctx Context) (string, bool) { + namespace, ok := ctx.Value(namespaceKey).(string) + return namespace, ok +} + +// ValidNamespaceOnCreateOrUpdate returns false if the namespace on the context differs from the resource. If the resource has no namespace, it is set to the value in the context. +func ValidNamespaceOnCreateOrUpdate(ctx Context, resource *JSONBase) bool { + ns, ok := NamespaceFrom(ctx) + if len(resource.Namespace) == 0 { + resource.Namespace = ns + } + return ns == resource.Namespace && ok +} diff --git a/pkg/api/context_test.go b/pkg/api/context_test.go new file mode 100644 index 00000000000..f6d9d4ccdb9 --- /dev/null +++ b/pkg/api/context_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api_test + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +// TestNamespaceContext validates that a namespace can be get/set on a context object +func TestNamespaceContext(t *testing.T) { + ctx := api.NewDefaultContext() + result, ok := api.NamespaceFrom(ctx) + if !ok { + t.Errorf("Error getting namespace") + } + if api.NamespaceDefault != result { + t.Errorf("Expected: %v, Actual: %v", api.NamespaceDefault, result) + } +} + +func TestValidNamespaceOnCreateOrUpdate(t *testing.T) { + ctx := api.NewDefaultContext() + namespace, _ := api.NamespaceFrom(ctx) + resource := api.ReplicationController{} + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &resource.JSONBase) { + t.Errorf("expected success") + } + if namespace != resource.Namespace { + t.Errorf("expected resource to have the default namespace assigned during validation") + } + resource = api.ReplicationController{JSONBase: api.JSONBase{Namespace: "other"}} + if api.ValidNamespaceOnCreateOrUpdate(ctx, &resource.JSONBase) { + t.Errorf("Expected error that resource and context errors do not match") + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index a081f0a7e12..fc3637f1c31 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -237,8 +237,16 @@ type JSONBase struct { SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Namespace string `json:"namespace",omitempty" yaml:"namespace,omitempty"` } +const ( + // NamespaceDefault means the object is in the default namespace which is applied when not specified by clients + NamespaceDefault string = "default" + // NamespaceAll is the default argument to specify on a context when you want to list or filter resources across all namespaces + NamespaceAll string = "" +) + // PodStatus represents a status of a pod. type PodStatus string diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 12131d35ccf..cc6addfdc49 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -248,6 +248,7 @@ type JSONBase struct { SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Namespace string `json:"namespace",omitempty" yaml:"namespace,omitempty"` } func (*JSONBase) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 7f320f7c807..359921f1ed0 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -246,6 +246,7 @@ type JSONBase struct { SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Namespace string `json:"namespace",omitempty" yaml:"namespace,omitempty"` } // PodStatus represents a status of a pod. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index d209e3c8790..61151150402 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -313,6 +313,7 @@ func ValidatePod(pod *api.Pod) errs.ErrorList { if len(pod.ID) == 0 { allErrs = append(allErrs, errs.NewFieldRequired("id", pod.ID)) } + allErrs = append(allErrs, validateNotEmptyDNSSubdomain(pod.Namespace, "pod.Namespace")...) allErrs = append(allErrs, ValidatePodState(&pod.DesiredState).Prefix("desiredState")...) return allErrs } @@ -325,6 +326,7 @@ func ValidateService(service *api.Service) errs.ErrorList { } else if !util.IsDNS952Label(service.ID) { allErrs = append(allErrs, errs.NewFieldInvalid("id", service.ID)) } + allErrs = append(allErrs, validateNotEmptyDNSSubdomain(service.Namespace, "service.Namespace")...) if !util.IsValidPortNum(service.Port) { allErrs = append(allErrs, errs.NewFieldInvalid("Service.Port", service.Port)) } @@ -345,6 +347,7 @@ func ValidateReplicationController(controller *api.ReplicationController) errs.E if len(controller.ID) == 0 { allErrs = append(allErrs, errs.NewFieldRequired("id", controller.ID)) } + allErrs = append(allErrs, validateNotEmptyDNSSubdomain(controller.Namespace, "controller.Namespace")...) allErrs = append(allErrs, ValidateReplicationControllerState(&controller.DesiredState).Prefix("desiredState")...) return allErrs } @@ -366,3 +369,14 @@ func ValidateReplicationControllerState(state *api.ReplicationControllerState) e allErrs = append(allErrs, ValidateManifest(&state.PodTemplate.DesiredState.Manifest).Prefix("podTemplate.desiredState.manifest")...) return allErrs } + +// validateNotEmptyDNSSubdomain validates the provided value is not empty and is a dns subdomain. +func validateNotEmptyDNSSubdomain(value string, label string) errs.ErrorList { + allErrs := errs.ErrorList{} + if value == "" { + allErrs = append(allErrs, errs.NewFieldInvalid(label, value)) + } else if !util.IsDNSSubdomain(value) { + allErrs = append(allErrs, errs.NewFieldInvalid(label, value)) + } + return allErrs +} diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 50ec2cd97bc..e2c7457d3c7 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -100,7 +100,8 @@ func curry(f func(runtime.Object, *http.Request) error, req *http.Request) func( // timeout= Timeout for synchronous requests, only applies if sync=true // labels= Used for filtering list operations func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w http.ResponseWriter, storage RESTStorage) { - ctx := api.NewContext() + // TODO for now, we perform all operations in the default namespace + ctx := api.NewDefaultContext() sync := req.URL.Query().Get("sync") == "true" timeout := parseTimeout(req.URL.Query().Get("timeout")) switch req.Method { diff --git a/pkg/registry/controller/rest.go b/pkg/registry/controller/rest.go index 2665be976d6..6a3aa7194e1 100644 --- a/pkg/registry/controller/rest.go +++ b/pkg/registry/controller/rest.go @@ -17,6 +17,7 @@ limitations under the License. package controller import ( + stderrs "errors" "fmt" "time" @@ -59,6 +60,10 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan runtime.Obje if !ok { return nil, fmt.Errorf("not a replication controller: %#v", obj) } + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &controller.JSONBase) { + return nil, errors.NewConflict("controller", controller.Namespace, stderrs.New("Controller.Namespace does not match the provided context")) + } + if len(controller.ID) == 0 { controller.ID = uuid.NewUUID().String() } @@ -128,6 +133,9 @@ func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan runtime.Obje if !ok { return nil, fmt.Errorf("not a replication controller: %#v", obj) } + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &controller.JSONBase) { + return nil, errors.NewConflict("controller", controller.Namespace, stderrs.New("Controller.Namespace does not match the provided context")) + } if errs := validation.ValidateReplicationController(controller); len(errs) > 0 { return nil, errors.NewInvalid("replicationController", controller.ID, errs) } diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 8f3919aaf4c..89416130f04 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -17,6 +17,7 @@ limitations under the License. package pod import ( + stderrs "errors" "fmt" "sync" "time" @@ -69,6 +70,9 @@ func NewREST(config *RESTConfig) *REST { func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { pod := obj.(*api.Pod) + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &pod.JSONBase) { + return nil, errors.NewConflict("pod", pod.Namespace, stderrs.New("Pod.Namespace does not match the provided context")) + } pod.DesiredState.Manifest.UUID = uuid.NewUUID().String() if len(pod.ID) == 0 { pod.ID = pod.DesiredState.Manifest.UUID @@ -77,7 +81,6 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan runtime.Obje if errs := validation.ValidatePod(pod); len(errs) > 0 { return nil, errors.NewInvalid("pod", pod.ID, errs) } - pod.CreationTimestamp = util.Now() return apiserver.MakeAsync(func() (runtime.Object, error) { @@ -159,6 +162,9 @@ func (*REST) New() runtime.Object { func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { pod := obj.(*api.Pod) + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &pod.JSONBase) { + return nil, errors.NewConflict("pod", pod.Namespace, stderrs.New("Pod.Namespace does not match the provided context")) + } if errs := validation.ValidatePod(pod); len(errs) > 0 { return nil, errors.NewInvalid("pod", pod.ID, errs) } diff --git a/pkg/registry/service/rest.go b/pkg/registry/service/rest.go index 0394ece8cc5..2f430435f39 100644 --- a/pkg/registry/service/rest.go +++ b/pkg/registry/service/rest.go @@ -17,6 +17,7 @@ limitations under the License. package service import ( + stderrs "errors" "fmt" "math/rand" "strconv" @@ -52,6 +53,9 @@ func NewREST(registry Registry, cloud cloudprovider.Interface, machines minion.R func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { srv := obj.(*api.Service) + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &srv.JSONBase) { + return nil, errors.NewConflict("service", srv.Namespace, stderrs.New("Service.Namespace does not match the provided context")) + } if errs := validation.ValidateService(srv); len(errs) > 0 { return nil, errors.NewInvalid("service", srv.ID, errs) } @@ -165,6 +169,9 @@ func GetServiceEnvironmentVariables(ctx api.Context, registry Registry, machine func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { srv := obj.(*api.Service) + if !api.ValidNamespaceOnCreateOrUpdate(ctx, &srv.JSONBase) { + return nil, errors.NewConflict("service", srv.Namespace, stderrs.New("Service.Namespace does not match the provided context")) + } if errs := validation.ValidateService(srv); len(errs) > 0 { return nil, errors.NewInvalid("service", srv.ID, errs) }