diff --git a/cluster/vagrant/provision-master.sh b/cluster/vagrant/provision-master.sh index 8b51af885fc..f654d7ed85a 100755 --- a/cluster/vagrant/provision-master.sh +++ b/cluster/vagrant/provision-master.sh @@ -75,7 +75,7 @@ grains: cloud_provider: vagrant roles: - kubernetes-master - admission_control: AlwaysAdmit + admission_control: NamespaceExists,AlwaysAdmit runtime_config: '$(echo "$RUNTIME_CONFIG" | sed -e "s/'/''/g")' EOF diff --git a/cmd/kubecfg/kubecfg.go b/cmd/kubecfg/kubecfg.go index 4b102c44d89..1bfb1c521a9 100644 --- a/cmd/kubecfg/kubecfg.go +++ b/cmd/kubecfg/kubecfg.go @@ -379,7 +379,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool { glog.Fatalf("usage: kubecfg [OPTIONS] %s <%s>", method, prettyWireStorage()) } case "update": - obj, err := c.Verb("GET").Namespace(api.Namespace(ctx)).Suffix(path).Do().Get() + obj, err := c.Verb("GET").Namespace(api.NamespaceValue(ctx)).Suffix(path).Do().Get() if err != nil { glog.Fatalf("error obtaining resource version for update: %v", err) } @@ -405,7 +405,7 @@ func executeAPIRequest(ctx api.Context, method string, c *client.Client) bool { return false } - r := c.Verb(verb).Namespace(api.Namespace(ctx)).Suffix(path) + r := c.Verb(verb).Namespace(api.NamespaceValue(ctx)).Suffix(path) if len(*selector) > 0 { r.ParseSelectorParam("labels", *selector) } diff --git a/examples/kubernetes-namespaces/README.md b/examples/kubernetes-namespaces/README.md new file mode 100644 index 00000000000..6557b18905c --- /dev/null +++ b/examples/kubernetes-namespaces/README.md @@ -0,0 +1,191 @@ +## Kubernetes Namespaces + +Kubernetes Namespaces help different projects, teams, or customers to share a Kubernetes cluster. + +It does this by providing the following: + +1. A scope for [Names](identifiers.md). +2. A mechanism to attach authorization and policy to a subsection of the cluster. + +Use of multiple namespaces is optional. + +This example demonstrates how to use Kubernetes namespaces to subdivide your cluster. + +### Step Zero: Prerequisites + +This example assumes the following: + +1. You have an existing Kubernetes cluster. +2. You have a basic understanding of Kubernetes pods, services, and replication controllers. + +### Step One: Understand the default namespace + +By default, a Kubernetes cluster will instantiate a default namespace when provisioning the cluster to hold the default set of pods, +services, and replication controllers used by the cluster. + +Assuming you have a fresh cluster, you can introspect the available namespace's by doing the following: + +```shell +$ cluster/kubectl.sh get namespaces +NAME LABELS +default +``` + +### Step Two: Create new namespaces + +For this exercise, we will create two additional Kubernetes namespaces to hold our content. + +Let's imagine a scenario where an organization is using a shared Kubernetes cluster for development and production use cases. + +The development team would like to maintain a space in the cluster where they can get a view on the list of pods, services, and replication-controllers +they use to build and run their application. In this space, Kubernetes resources come and go, and the restrictions on who can or cannot modify resources +are relaxed to enable agile development. + +The operations team would like to maintain a space in the cluster where they can enforce strict procedures on who can or cannot manipulate the set of +pods, services, and replication controllers that run the production site. + +One pattern this organization could follow is to partition the Kubernetes cluster into two namespaces: development and production. + +Let's create two new namespaces to hold our work. + +Use the file `examples/kubernetes-namespaces/namespace-dev.json` which describes a development namespace: + +```js +{ + "kind": "Namespace", + "apiVersion":"v1beta1", + "id": "development", + "spec": {}, + "status": {}, + "labels": { + "name": "development" + }, +} +``` + +Create the development namespace using kubectl. + +```shell +$ cluster/kubectl.sh create -f examples/kubernetes-namespaces/namespace-dev.json +``` + +And then lets create the production namespace using kubectl. + +```shell +$ cluster/kubectl.sh create -f examples/kubernetes-namespaces/namespace-prod.json +``` + +To be sure things are right, let's list all of the namespaces in our cluster. + +```shell +$ cluster/kubectl.sh get namespaces +NAME LABELS +default +development name=development +production name=production +``` + +### Step Three: Create pods in each namespace + +A Kubernetes namespace provides the scope for pods, services, and replication controllers in the cluster. + +Users interacting with one namespace do not see the content in another namespace. + +To demonstrate this, let's spin up a simple replication controller and pod in the development namespace. + +The first step is to define a context for the kubectl client to work in each namespace. + +```shell +$ cluster/kubectl.sh config set-context dev --namespace=development +$ cluster/kubectl.sh config set-context prod --namespace=production +``` + +The above commands provided two request contexts you can alternate against depending on what namespace you +wish to work against. + +Let's switch to operate in the development namespace. + +```shell +$ cluster/kubectl.sh config use-context dev +``` + +You can verify your current context by doing the following: + +```shell +$ cluster/kubectl.sh config view +clusters: {} +contexts: + dev: + cluster: "" + namespace: development + user: "" + prod: + cluster: "" + namespace: production + user: "" +current-context: dev +preferences: {} +users: {} +``` + +At this point, all requests we make to the Kubernetes cluster from the command line are scoped to the development namespace. + +Let's create some content. + +```shell +$ cluster/kubectl.sh run-container snowflake --image=kubernetes/serve_hostname --replicas=2 +``` + +We have just created a replication controller whose replica size is 2 that is running the pod called snowflake with a basic container that just serves the hostname. + +```shell +cluster/kubectl.sh get rc +CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS +snowflake snowflake kubernetes/serve_hostname run-container=snowflake 2 + +$ cluster/kubectl.sh get pods +POD IP CONTAINER(S) IMAGE(S) HOST LABELS STATUS +snowflake-fplln 10.246.0.5 snowflake kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=snowflake Running +snowflake-gziey 10.246.0.4 snowflake kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=snowflake Running +``` + +And this is great, developers are able to do what they want, and they do not have to worry about affecting content in the production namespace. + +Let's switch to the production namespace and show how resources in one namespace are hidden from the other. + +```shell +$ cluster/kubectl.sh config use-context prod +``` + +The production namespace should be empty. + +```shell +$ cluster/kubectl.sh get rc +CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS + +$ cluster/kubectl.sh get pods +POD IP CONTAINER(S) IMAGE(S) HOST LABELS STATUS +``` + +Production likes to run cattle, so let's create some cattle pods. + +```shell +$ cluster/kubectl.sh run-container cattle --image=kubernetes/serve_hostname --replicas=5 + +$ cluster/kubectl.sh get rc +CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS +cattle cattle kubernetes/serve_hostname run-container=cattle 5 + +$ cluster/kubectl.sh get pods +POD IP CONTAINER(S) IMAGE(S) HOST LABELS STATUS +cattle-0133o 10.246.0.7 cattle kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=cattle Running +cattle-hh2gd 10.246.0.10 cattle kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=cattle Running +cattle-ls6k1 10.246.0.9 cattle kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=cattle Running +cattle-nyxxv 10.246.0.8 cattle kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=cattle Running +cattle-oh43e 10.246.0.6 cattle kubernetes/serve_hostname 10.245.1.3/10.245.1.3 run-container=cattle Running +``` + +At this point, it should be clear that the resources users create in one namespace are hidden from the other namespace. + +As the policy support in Kubernetes evolves, we will extend this scenario to show how you can provide different +authorization rules for each namespace. diff --git a/examples/kubernetes-namespaces/namespace-dev.json b/examples/kubernetes-namespaces/namespace-dev.json new file mode 100644 index 00000000000..12d7da4b7a4 --- /dev/null +++ b/examples/kubernetes-namespaces/namespace-dev.json @@ -0,0 +1,10 @@ +{ + "kind": "Namespace", + "apiVersion":"v1beta1", + "id": "development", + "spec": {}, + "status": {}, + "labels": { + "name": "development" + }, +} diff --git a/examples/kubernetes-namespaces/namespace-prod.json b/examples/kubernetes-namespaces/namespace-prod.json new file mode 100644 index 00000000000..9f3e49b8d6a --- /dev/null +++ b/examples/kubernetes-namespaces/namespace-prod.json @@ -0,0 +1,10 @@ +{ + "kind": "Namespace", + "apiVersion":"v1beta1", + "id": "production", + "spec": {}, + "status": {}, + "labels": { + "name": "production" + }, +} diff --git a/pkg/api/context.go b/pkg/api/context.go index 84a946d02c8..6206b7c6eda 100644 --- a/pkg/api/context.go +++ b/pkg/api/context.go @@ -63,8 +63,8 @@ func NamespaceFrom(ctx Context) (string, bool) { return namespace, ok } -// Namespace returns the value of the namespace key on the ctx, or the empty string if none -func Namespace(ctx Context) string { +// NamespaceValue returns the value of the namespace key on the ctx, or the empty string if none +func NamespaceValue(ctx Context) string { namespace, _ := NamespaceFrom(ctx) return namespace } diff --git a/pkg/api/context_test.go b/pkg/api/context_test.go index a463ffe2f35..70c6b4d5ba6 100644 --- a/pkg/api/context_test.go +++ b/pkg/api/context_test.go @@ -61,7 +61,7 @@ func TestValidNamespace(t *testing.T) { } ctx = api.NewContext() - ns := api.Namespace(ctx) + ns := api.NamespaceValue(ctx) if ns != "" { t.Errorf("Expected the empty string") } diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index 519c270e340..012ee85b765 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -122,8 +122,9 @@ func init() { // the list of kinds that are scoped at the root of the api hierarchy // if a kind is not enumerated here, it is assumed to have a namespace scope kindToRootScope := map[string]bool{ - "Node": true, - "Minion": true, + "Node": true, + "Minion": true, + "Namespace": true, } // enumerate all supported versions, get the kinds, and register with the mapper how to address our resources diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go index bf9fd70f832..1059686912f 100644 --- a/pkg/api/meta/restmapper.go +++ b/pkg/api/meta/restmapper.go @@ -51,7 +51,7 @@ var RESTScopeNamespaceLegacy = &restScope{ var RESTScopeNamespace = &restScope{ name: RESTScopeNameNamespace, - paramName: "ns", + paramName: "namespaces", paramPath: true, paramDescription: "object name and auth scope, such as for teams and projects", } diff --git a/pkg/api/register.go b/pkg/api/register.go index 8901111bb31..f640317948a 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -50,6 +50,8 @@ func init() { &ResourceQuota{}, &ResourceQuotaList{}, &ResourceQuotaUsage{}, + &Namespace{}, + &NamespaceList{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -81,3 +83,5 @@ func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} func (*ResourceQuotaUsage) IsAnAPIObject() {} +func (*Namespace) IsAnAPIObject() {} +func (*NamespaceList) IsAnAPIObject() {} diff --git a/pkg/api/rest/types.go b/pkg/api/rest/types.go index 5c4ccf90edf..155aca37da6 100644 --- a/pkg/api/rest/types.go +++ b/pkg/api/rest/types.go @@ -118,7 +118,7 @@ type nodeStrategy struct { // objects. var Nodes RESTCreateStrategy = nodeStrategy{api.Scheme, api.SimpleNameGenerator} -// NamespaceScoped is false for services. +// NamespaceScoped is false for nodes. func (nodeStrategy) NamespaceScoped() bool { return false } @@ -134,3 +134,30 @@ func (nodeStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { node := obj.(*api.Node) return validation.ValidateMinion(node) } + +// namespaceStrategy implements behavior for nodes +type namespaceStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Namespaces is the default logic that applies when creating and updating Namespace +// objects. +var Namespaces RESTCreateStrategy = namespaceStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is false for namespaces. +func (namespaceStrategy) NamespaceScoped() bool { + return false +} + +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (namespaceStrategy) ResetBeforeCreate(obj runtime.Object) { + _ = obj.(*api.Namespace) + // Namespace allow *all* fields, including status, to be set. +} + +// Validate validates a new namespace. +func (namespaceStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { + namespace := obj.(*api.Namespace) + return validation.ValidateNamespace(namespace) +} diff --git a/pkg/api/types.go b/pkg/api/types.go index d4ad537cf87..e841d4b8301 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -851,6 +851,35 @@ type NodeList struct { Items []Node `json:"items"` } +// NamespaceSpec describes the attributes on a Namespace +type NamespaceSpec struct { +} + +// NamespaceStatus is information about the current status of a Namespace. +type NamespaceStatus struct { +} + +// A namespace provides a scope for Names. +// Use of multiple namespaces is optional +type Namespace struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the behavior of the Namespace. + Spec NamespaceSpec `json:"spec,omitempty"` + + // Status describes the current status of a Namespace + Status NamespaceStatus `json:"status,omitempty"` +} + +// NamespaceList is a list of Namespaces. +type NamespaceList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + Items []Namespace `json:"items"` +} + // Binding is written by a scheduler to cause a pod to be bound to a host. type Binding struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 3747faa83b7..63edaeddc61 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -743,6 +743,21 @@ func init() { } return nil }, + func(in *Namespace, out *newer.Namespace, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { + return err + } + return nil + }, func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { *out = LimitRangeSpec{} out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index d3f8a91a651..89a6ba88322 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -51,6 +51,8 @@ func init() { &ResourceQuota{}, &ResourceQuotaList{}, &ResourceQuotaUsage{}, + &Namespace{}, + &NamespaceList{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -82,3 +84,5 @@ func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} func (*ResourceQuotaUsage) IsAnAPIObject() {} +func (*Namespace) IsAnAPIObject() {} +func (*NamespaceList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index f164dc74f37..ab6a5651200 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -684,6 +684,36 @@ type MinionList struct { Items []Minion `json:"items" description:"list of nodes"` } +// NamespaceSpec describes the attributes on a Namespace +type NamespaceSpec struct { +} + +// NamespaceStatus is information about the current status of a Namespace. +type NamespaceStatus struct { +} + +// A namespace provides a scope for Names. +// Use of multiple namespaces is optional +type Namespace struct { + TypeMeta `json:",inline"` + + // Labels + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize namespaces"` + + // Spec defines the behavior of the Namespace. + Spec NamespaceSpec `json:"spec,omitempty"` + + // Status describes the current status of a Namespace + Status NamespaceStatus `json:"status,omitempty"` +} + +// NamespaceList is a list of Namespaces. +type NamespaceList struct { + TypeMeta `json:",inline"` + + Items []Namespace `json:"items"` +} + // Binding is written by a scheduler to cause a pod to be bound to a host. type Binding struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 627b8463b1f..7e8b662a882 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -663,6 +663,21 @@ func init() { } return nil }, + func(in *Namespace, out *newer.Namespace, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { + return err + } + return nil + }, func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { *out = LimitRangeSpec{} out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 88fe96e700d..c803a01e42b 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -51,6 +51,8 @@ func init() { &ResourceQuota{}, &ResourceQuotaList{}, &ResourceQuotaUsage{}, + &Namespace{}, + &NamespaceList{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -82,3 +84,5 @@ func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} func (*ResourceQuotaUsage) IsAnAPIObject() {} +func (*Namespace) IsAnAPIObject() {} +func (*NamespaceList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index e49fb778ca2..25ac7b3ac52 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -645,6 +645,36 @@ type MinionList struct { Items []Minion `json:"items" description:"list of nodes"` } +// NamespaceSpec describes the attributes on a Namespace +type NamespaceSpec struct { +} + +// NamespaceStatus is information about the current status of a Namespace. +type NamespaceStatus struct { +} + +// A namespace provides a scope for Names. +// Use of multiple namespaces is optional +type Namespace struct { + TypeMeta `json:",inline"` + + // Labels + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize namespaces"` + + // Spec defines the behavior of the Namespace. + Spec NamespaceSpec `json:"spec,omitempty"` + + // Status describes the current status of a Namespace + Status NamespaceStatus `json:"status,omitempty"` +} + +// NamespaceList is a list of Namespaces. +type NamespaceList struct { + TypeMeta `json:",inline"` + + Items []Namespace `json:"items"` +} + // Binding is written by a scheduler to cause a pod to be bound to a host. type Binding struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index da801ab73b0..65106f9ce6b 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -51,6 +51,8 @@ func init() { &ResourceQuota{}, &ResourceQuotaList{}, &ResourceQuotaUsage{}, + &Namespace{}, + &NamespaceList{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -82,3 +84,5 @@ func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} func (*ResourceQuotaUsage) IsAnAPIObject() {} +func (*Namespace) IsAnAPIObject() {} +func (*NamespaceList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index b28f78bcb8d..844e20d121a 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -866,6 +866,35 @@ type NodeList struct { Items []Node `json:"items"` } +// NamespaceSpec describes the attributes on a Namespace +type NamespaceSpec struct { +} + +// NamespaceStatus is information about the current status of a Namespace. +type NamespaceStatus struct { +} + +// A namespace provides a scope for Names. +// Use of multiple namespaces is optional +type Namespace struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the behavior of the Namespace. + Spec NamespaceSpec `json:"spec,omitempty"` + + // Status describes the current status of a Namespace + Status NamespaceStatus `json:"status,omitempty"` +} + +// NamespaceList is a list of Namespaces. +type NamespaceList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + Items []Namespace `json:"items"` +} + // Binding is written by a scheduler to cause a pod to be bound to a node. Name is not // required for Bindings. type Binding struct { diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 78427fa2f2f..9eaa9b4aa0f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -109,6 +109,13 @@ func ValidateNodeName(name string, prefix bool) (bool, string) { return nameIsDNSSubdomain(name, prefix) } +// ValidateNamespaceName can be used to check whether the given namespace name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +func ValidateNamespaceName(name string, prefix bool) (bool, string) { + return nameIsDNSSubdomain(name, prefix) +} + // nameIsDNSSubdomain is a ValidateNameFunc for names that must be a DNS subdomain. func nameIsDNSSubdomain(name string, prefix bool) (bool, string) { if prefix { @@ -839,3 +846,27 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro } return allErrs } + +// ValidateNamespace tests if required fields are set. +func ValidateNamespace(namespace *api.Namespace) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&namespace.ObjectMeta, false, ValidateNamespaceName).Prefix("metadata")...) + return allErrs +} + +// ValidateNamespaceUpdate tests to make sure a mamespace update can be applied. Modifies oldNamespace. +func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespace) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldNamespace.ObjectMeta, &namespace.ObjectMeta).Prefix("metadata")...) + + // TODO: move reset function to its own location + // Ignore metadata changes now that they have been tested + oldNamespace.ObjectMeta = namespace.ObjectMeta + + // TODO: Add a 'real' ValidationError type for this error and provide print actual diffs. + if !api.Semantic.DeepEqual(oldNamespace, namespace) { + glog.V(4).Infof("Update failed validation %#v vs %#v", oldNamespace, namespace) + allErrs = append(allErrs, fmt.Errorf("update contains more than labels or annotation changes")) + } + return allErrs +} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 59c04ecece2..e19e90c568d 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2353,3 +2353,114 @@ func TestValidateResourceQuota(t *testing.T) { } } } + +func TestValidateNamespace(t *testing.T) { + validLabels := map[string]string{"a": "b"} + invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} + successCases := []api.Namespace{ + { + ObjectMeta: api.ObjectMeta{Name: "abc", Labels: validLabels}, + }, + { + ObjectMeta: api.ObjectMeta{Name: "abc-123"}, + }, + } + for _, successCase := range successCases { + if errs := ValidateNamespace(&successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := map[string]struct { + R api.Namespace + D string + }{ + "zero-length name": { + api.Namespace{ObjectMeta: api.ObjectMeta{Name: ""}}, + "", + }, + "defined-namespace": { + api.Namespace{ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: "makesnosense"}}, + "", + }, + "invalid-labels": { + api.Namespace{ObjectMeta: api.ObjectMeta{Name: "abc", Labels: invalidLabels}}, + "", + }, + } + for k, v := range errorCases { + errs := ValidateNamespace(&v.R) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + } +} + +func TestValidateNamespaceUpdate(t *testing.T) { + tests := []struct { + oldNamespace api.Namespace + namespace api.Namespace + valid bool + }{ + {api.Namespace{}, api.Namespace{}, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}}, + api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "bar"}, + }, false}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar"}, + }, + }, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "baz"}, + }, + }, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + }, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "baz"}, + }, + }, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"bar": "foo"}, + }, + }, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "baz"}, + }, + }, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "baz"}, + }, + }, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"Foo": "baz"}, + }, + }, false}, + } + for i, test := range tests { + errs := ValidateNamespaceUpdate(&test.oldNamespace, &test.namespace) + if test.valid && len(errs) > 0 { + t.Errorf("%d: Unexpected error: %v", i, errs) + t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta) + } + if !test.valid && len(errs) == 0 { + t.Errorf("%d: Unexpected non-error", i) + } + } +} diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index a04a57a81e7..2adca83fbce 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -232,7 +232,7 @@ func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Sel storage.requestedLabelSelector = label storage.requestedFieldSelector = field storage.requestedResourceVersion = resourceVersion - storage.requestedResourceNamespace = api.Namespace(ctx) + storage.requestedResourceNamespace = api.NamespaceValue(ctx) if err := storage.errors["watch"]; err != nil { return nil, err } @@ -243,7 +243,7 @@ func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Sel // Implement Redirector. func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) (string, error) { // validate that the namespace context on the request matches the expected input - storage.requestedResourceNamespace = api.Namespace(ctx) + storage.requestedResourceNamespace = api.NamespaceValue(ctx) if storage.expectedResourceNamespace != storage.requestedResourceNamespace { return "", fmt.Errorf("Expected request namespace %s, but got namespace %s", storage.expectedResourceNamespace, storage.requestedResourceNamespace) } diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 01ceab40ad3..8bf0df9020b 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -223,8 +223,10 @@ type APIRequestInfoResolver struct { // GetAPIRequestInfo returns the information from the http request. If error is not nil, APIRequestInfo holds the information as best it is known before the failure // Valid Inputs: // Storage paths -// /ns/{namespace}/{resource} -// /ns/{namespace}/{resource}/{resourceName} +// /namespaces +// /namespaces/{namespace} +// /namespaces/{namespace}/{resource} +// /namespaces/{namespace}/{resource}/{resourceName} // /{resource} // /{resource}/{resourceName} // /{resource}/{resourceName}?namespace={namespace} @@ -287,15 +289,16 @@ func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIReques } - // URL forms: /ns/{namespace}/{resource}/*, where parts are adjusted to be relative to kind - if currentParts[0] == "ns" { + // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind + if currentParts[0] == "namespaces" { if len(currentParts) < 3 { - return requestInfo, fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*") + requestInfo.Namespace = "" + requestInfo.Resource = "namespaces" + } else { + requestInfo.Resource = currentParts[2] + requestInfo.Namespace = currentParts[1] + currentParts = currentParts[2:] } - requestInfo.Resource = currentParts[2] - requestInfo.Namespace = currentParts[1] - currentParts = currentParts[2:] - } else { // URL forms: /{resource}/* // URL forms: POST /{resource} is a legacy API convention to create in "default" namespace diff --git a/pkg/apiserver/handlers_test.go b/pkg/apiserver/handlers_test.go index 3cc5b05d685..5e969476e6b 100644 --- a/pkg/apiserver/handlers_test.go +++ b/pkg/apiserver/handlers_test.go @@ -77,9 +77,13 @@ func TestGetAPIRequestInfo(t *testing.T) { expectedName string expectedParts []string }{ + // resource paths - {"GET", "/ns/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, - {"GET", "/ns/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/namespaces", "list", "", "", "namespaces", "Namespace", "", []string{"namespaces"}}, + {"GET", "/namespaces/other", "get", "", "", "namespaces", "Namespace", "other", []string{"namespaces", "other"}}, + + {"GET", "/namespaces/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/pods", "list", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, {"POST", "/pods", "create", "", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}}, {"GET", "/pods/foo", "get", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, @@ -87,16 +91,16 @@ func TestGetAPIRequestInfo(t *testing.T) { {"GET", "/pods?namespace=other", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, // special verbs - {"GET", "/proxy/ns/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/proxy/namespaces/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/proxy/pods/foo", "proxy", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", "/redirect/ns/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/redirect/namespaces/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/redirect/pods/foo", "redirect", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, - {"GET", "/watch/ns/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/watch/namespaces/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}}, // fully-qualified paths - {"GET", "/api/v1beta1/ns/other/pods", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, - {"GET", "/api/v1beta1/ns/other/pods/foo", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/namespaces/other/pods", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/namespaces/other/pods/foo", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, {"POST", "/api/v1beta1/pods", "create", "v1beta1", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}}, {"GET", "/api/v1beta1/pods/foo", "get", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, @@ -105,7 +109,7 @@ func TestGetAPIRequestInfo(t *testing.T) { {"GET", "/api/v1beta1/proxy/pods/foo", "proxy", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/redirect/pods/foo", "redirect", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/watch/pods", "watch", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, - {"GET", "/api/v1beta1/watch/ns/other/pods", "watch", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/watch/namespaces/other/pods", "watch", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, } apiRequestInfoResolver := &APIRequestInfoResolver{util.NewStringSet("api"), latest.RESTMapper} @@ -141,11 +145,9 @@ func TestGetAPIRequestInfo(t *testing.T) { } errorCases := map[string]string{ - "no resource path": "/", - "missing resource type": "/ns/other", - "just apiversion": "/api/v1beta1/", - "apiversion with no resource": "/api/v1beta1/", - "apiversion with just namespace": "/api/v1beta1/ns/other", + "no resource path": "/", + "just apiversion": "/api/v1beta1/", + "apiversion with no resource": "/api/v1beta1/", } for k, v := range errorCases { req, err := http.NewRequest("GET", v, nil) diff --git a/pkg/apiserver/proxy_test.go b/pkg/apiserver/proxy_test.go index fe809d15553..334391e2199 100644 --- a/pkg/apiserver/proxy_test.go +++ b/pkg/apiserver/proxy_test.go @@ -294,7 +294,7 @@ func TestProxy(t *testing.T) { server *httptest.Server proxyTestPattern string }{ - {namespaceServer, "/prefix/version/proxy/ns/" + item.reqNamespace + "/foo/id" + item.path}, + {namespaceServer, "/prefix/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, {legacyNamespaceServer, "/prefix/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, } diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index a0a1c4cf2f2..4c2946e7013 100644 --- a/pkg/apiserver/redirect_test.go +++ b/pkg/apiserver/redirect_test.go @@ -107,7 +107,7 @@ func TestRedirectWithNamespaces(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = item.id - resp, err := client.Get(server.URL + "/prefix/version/redirect/ns/other/foo/" + item.id) + resp, err := client.Get(server.URL + "/prefix/version/redirect/namespaces/other/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 22678f4c900..10d993c755a 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -45,6 +45,7 @@ type RESTHandler struct { func (h *RESTHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { + glog.Errorf("Unable to handle request %s %s %v", requestInfo.Namespace, requestInfo.Kind, err) notFound(w, req) return } diff --git a/pkg/client/client.go b/pkg/client/client.go index 6fddce33a20..273e77e4395 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -40,6 +40,7 @@ type Interface interface { LimitRangesNamespacer ResourceQuotasNamespacer ResourceQuotaUsagesNamespacer + NamespacesInterface } func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface { @@ -78,6 +79,10 @@ func (c *Client) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterfa return newResourceQuotaUsages(c, namespace) } +func (c *Client) Namespaces() NamespaceInterface { + return newNamespaces(c) +} + // VersionInterface has a method to retrieve the server version. type VersionInterface interface { ServerVersion() (*version.Info, error) diff --git a/pkg/client/fake.go b/pkg/client/fake.go index 42bd2e5f36d..28269cbebe1 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -44,6 +44,7 @@ type Fake struct { EventsList api.EventList LimitRangesList api.LimitRangeList ResourceQuotasList api.ResourceQuotaList + NamespacesList api.NamespaceList Err error Watch watch.Interface } @@ -84,6 +85,10 @@ func (c *Fake) Services(namespace string) ServiceInterface { return &FakeServices{Fake: c, Namespace: namespace} } +func (c *Fake) Namespaces() NamespaceInterface { + return &FakeNamespaces{Fake: c} +} + func (c *Fake) ServerVersion() (*version.Info, error) { c.Actions = append(c.Actions, FakeAction{Action: "get-version", Value: nil}) versionInfo := version.Get() diff --git a/pkg/client/fake_namespaces.go b/pkg/client/fake_namespaces.go new file mode 100644 index 00000000000..7e7f7ff086d --- /dev/null +++ b/pkg/client/fake_namespaces.go @@ -0,0 +1,53 @@ +/* +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 client + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +// FakeNamespaces implements NamespacesInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the methods you want to test easier. +type FakeNamespaces struct { + Fake *Fake +} + +func (c *FakeNamespaces) List(selector labels.Selector) (*api.NamespaceList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-namespaces"}) + return api.Scheme.CopyOrDie(&c.Fake.NamespacesList).(*api.NamespaceList), nil +} + +func (c *FakeNamespaces) Get(name string) (*api.Namespace, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-namespace", Value: name}) + return &api.Namespace{ObjectMeta: api.ObjectMeta{Name: name}}, nil +} + +func (c *FakeNamespaces) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-namespace", Value: name}) + return nil +} + +func (c *FakeNamespaces) Create(namespace *api.Namespace) (*api.Namespace, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-namespace"}) + return &api.Namespace{}, nil +} + +func (c *FakeNamespaces) Update(namespace *api.Namespace) (*api.Namespace, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-namespace", Value: namespace.Name}) + return &api.Namespace{}, nil +} diff --git a/pkg/client/namespaces.go b/pkg/client/namespaces.go new file mode 100644 index 00000000000..2611761c5ee --- /dev/null +++ b/pkg/client/namespaces.go @@ -0,0 +1,88 @@ +/* +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 client + +import ( + "errors" + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +type NamespacesInterface interface { + Namespaces() NamespaceInterface +} + +type NamespaceInterface interface { + Create(item *api.Namespace) (*api.Namespace, error) + Get(name string) (result *api.Namespace, err error) + List(selector labels.Selector) (*api.NamespaceList, error) + Delete(name string) error + Update(item *api.Namespace) (*api.Namespace, error) +} + +// namespaces implements NamespacesInterface +type namespaces struct { + r *Client +} + +// newNamespaces returns a namespaces object. +func newNamespaces(c *Client) *namespaces { + return &namespaces{r: c} +} + +// Create creates a new namespace. +func (c *namespaces) Create(namespace *api.Namespace) (*api.Namespace, error) { + result := &api.Namespace{} + err := c.r.Post().Resource("namespaces").Body(namespace).Do().Into(result) + return result, err +} + +// List lists all the namespaces in the cluster. +func (c *namespaces) List(selector labels.Selector) (*api.NamespaceList, error) { + result := &api.NamespaceList{} + err := c.r.Get().Resource("namespaces").SelectorParam("labels", selector).Do().Into(result) + return result, err +} + +// Update takes the representation of a namespace to update. Returns the server's representation of the namespace, and an error, if it occurs. +func (c *namespaces) Update(namespace *api.Namespace) (result *api.Namespace, err error) { + result = &api.Namespace{} + if len(namespace.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", namespace) + return + } + err = c.r.Put().Resource("namespaces").Name(namespace.Name).Body(namespace).Do().Into(result) + return +} + +// Get gets an existing namespace +func (c *namespaces) Get(name string) (*api.Namespace, error) { + if len(name) == 0 { + return nil, errors.New("name is required parameter to Get") + } + + result := &api.Namespace{} + err := c.r.Get().Resource("namespaces").Name(name).Do().Into(result) + return result, err +} + +// Delete deletes an existing namespace. +func (c *namespaces) Delete(name string) error { + return c.r.Delete().Resource("namespaces").Name(name).Do().Error() +} diff --git a/pkg/client/namespaces_test.go b/pkg/client/namespaces_test.go new file mode 100644 index 00000000000..c70c2f369bd --- /dev/null +++ b/pkg/client/namespaces_test.go @@ -0,0 +1,134 @@ +/* +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 client + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +func TestNamespaceCreate(t *testing.T) { + // we create a namespace relative to another namespace + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + } + c := &testClient{ + Request: testRequest{ + Method: "POST", + Path: "/namespaces", + Body: namespace, + }, + Response: Response{StatusCode: 200, Body: namespace}, + } + + // from the source ns, provision a new global namespace "foo" + response, err := c.Setup().Namespaces().Create(namespace) + + if err != nil { + t.Errorf("%#v should be nil.", err) + } + + if e, a := response.Name, namespace.Name; e != a { + t.Errorf("%#v != %#v.", e, a) + } +} + +func TestNamespaceGet(t *testing.T) { + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: "/namespaces/foo", + Body: nil, + }, + Response: Response{StatusCode: 200, Body: namespace}, + } + + response, err := c.Setup().Namespaces().Get("foo") + + if err != nil { + t.Errorf("%#v should be nil.", err) + } + + if e, r := response.Name, namespace.Name; e != r { + t.Errorf("%#v != %#v.", e, r) + } +} + +func TestNamespaceList(t *testing.T) { + namespaceList := &api.NamespaceList{ + Items: []api.Namespace{ + { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: "/ns", + Body: nil, + }, + Response: Response{StatusCode: 200, Body: namespaceList}, + } + response, err := c.Setup().Namespaces().List(labels.Everything()) + + if err != nil { + t.Errorf("%#v should be nil.", err) + } + + if len(response.Items) != 1 { + t.Errorf("%#v response.Items should have len 1.", response.Items) + } + + responseNamespace := response.Items[0] + if e, r := responseNamespace.Name, "foo"; e != r { + t.Errorf("%#v != %#v.", e, r) + } +} + +func TestNamespaceUpdate(t *testing.T) { + requestNamespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + ResourceVersion: "1", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: "/namespaces/foo"}, + Response: Response{StatusCode: 200, Body: requestNamespace}, + } + receivedNamespace, err := c.Setup().Namespaces().Update(requestNamespace) + c.Validate(t, receivedNamespace, err) +} + +func TestNamespaceDelete(t *testing.T) { + c := &testClient{ + Request: testRequest{Method: "DELETE", Path: "/namespaces/foo"}, + Response: Response{StatusCode: 200}, + } + err := c.Setup().Namespaces().Delete("foo") + c.Validate(t, nil, err) +} diff --git a/pkg/client/request.go b/pkg/client/request.go index e9b5b50f72c..3ea84e11f15 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -298,7 +298,7 @@ func (r *Request) Body(obj interface{}) *Request { func (r *Request) finalURL() string { p := r.path if r.namespaceSet && !r.namespaceInQuery && len(r.namespace) > 0 { - p = path.Join(p, "ns", r.namespace) + p = path.Join(p, "namespaces", r.namespace) } if len(r.resource) != 0 { resource := r.resource diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index bba74b8f19d..f4bdafe9538 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -112,7 +112,7 @@ func TestRequestSetsNamespace(t *testing.T) { Path: "/", }, }).Namespace("foo") - if s := r.finalURL(); s != "ns/foo" { + if s := r.finalURL(); s != "namespaces/foo" { t.Errorf("namespace should be in path: %s", s) } } @@ -122,7 +122,7 @@ func TestRequestOrdersNamespaceInPath(t *testing.T) { baseURL: &url.URL{}, path: "/test/", }).Name("bar").Resource("baz").Namespace("foo") - if s := r.finalURL(); s != "/test/ns/foo/baz/bar" { + if s := r.finalURL(); s != "/test/namespaces/foo/baz/bar" { t.Errorf("namespace should be in order in path: %s", s) } } diff --git a/pkg/kubecfg/kubecfg.go b/pkg/kubecfg/kubecfg.go index 0cdd866056d..a8f0a51f746 100644 --- a/pkg/kubecfg/kubecfg.go +++ b/pkg/kubecfg/kubecfg.go @@ -123,7 +123,7 @@ var ( // update of the image is performed. func Update(ctx api.Context, name string, client client.Interface, updatePeriod time.Duration, imageName string) error { // TODO ctx is not needed as input to this function, should just be 'namespace' - controller, err := client.ReplicationControllers(api.Namespace(ctx)).Get(name) + controller, err := client.ReplicationControllers(api.NamespaceValue(ctx)).Get(name) if err != nil { return err } @@ -138,7 +138,7 @@ func Update(ctx api.Context, name string, client client.Interface, updatePeriod s := labels.Set(controller.Spec.Selector).AsSelector() - podList, err := client.Pods(api.Namespace(ctx)).List(s) + podList, err := client.Pods(api.NamespaceValue(ctx)).List(s) if err != nil { return err } @@ -156,7 +156,7 @@ func Update(ctx api.Context, name string, client client.Interface, updatePeriod time.Sleep(updatePeriod) } return wait.Poll(updatePollInterval, updatePollTimeout, func() (bool, error) { - podList, err := client.Pods(api.Namespace(ctx)).List(s) + podList, err := client.Pods(api.NamespaceValue(ctx)).List(s) if err != nil { return false, err } @@ -172,12 +172,12 @@ func StopController(ctx api.Context, name string, client client.Interface) error // ResizeController resizes a controller named 'name' by setting replicas to 'replicas'. func ResizeController(ctx api.Context, name string, replicas int, client client.Interface) error { // TODO ctx is not needed, and should just be a namespace - controller, err := client.ReplicationControllers(api.Namespace(ctx)).Get(name) + controller, err := client.ReplicationControllers(api.NamespaceValue(ctx)).Get(name) if err != nil { return err } controller.Spec.Replicas = replicas - controllerOut, err := client.ReplicationControllers(api.Namespace(ctx)).Update(controller) + controllerOut, err := client.ReplicationControllers(api.NamespaceValue(ctx)).Update(controller) if err != nil { return err } @@ -270,7 +270,7 @@ func RunController(ctx api.Context, image, name string, replicas int, client cli }, } - controllerOut, err := client.ReplicationControllers(api.Namespace(ctx)).Create(controller) + controllerOut, err := client.ReplicationControllers(api.NamespaceValue(ctx)).Create(controller) if err != nil { return err } @@ -310,7 +310,7 @@ func createService(ctx api.Context, name string, port int, client client.Interfa }, }, } - svc, err := client.Services(api.Namespace(ctx)).Create(svc) + svc, err := client.Services(api.NamespaceValue(ctx)).Create(svc) return svc, err } @@ -318,12 +318,12 @@ func createService(ctx api.Context, name string, port int, client client.Interfa // already be stopped. func DeleteController(ctx api.Context, name string, client client.Interface) error { // TODO remove ctx in favor of just namespace string - controller, err := client.ReplicationControllers(api.Namespace(ctx)).Get(name) + controller, err := client.ReplicationControllers(api.NamespaceValue(ctx)).Get(name) if err != nil { return err } if controller.Spec.Replicas != 0 { return fmt.Errorf("controller has non-zero replicas (%d), please stop it first", controller.Spec.Replicas) } - return client.ReplicationControllers(api.Namespace(ctx)).Delete(name) + return client.ReplicationControllers(api.NamespaceValue(ctx)).Delete(name) } diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index 0142bedad0e..7f00d78268d 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -34,7 +34,7 @@ func TestCreateObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods" && m == "POST": + case p == "/namespaces/test/pods" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -64,9 +64,9 @@ func TestCreateMultipleObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods" && m == "POST": + case p == "/namespaces/test/pods" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/services" && m == "POST": + case p == "/namespaces/test/services" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -98,11 +98,11 @@ func TestCreateDirectory(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods" && m == "POST": + case p == "/namespaces/test/pods" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/services" && m == "POST": + case p == "/namespaces/test/services" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil - case p == "/ns/test/replicationcontrollers" && m == "POST": + case p == "/namespaces/test/replicationcontrollers" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) diff --git a/pkg/kubectl/cmd/delete_test.go b/pkg/kubectl/cmd/delete_test.go index 7fad141e7bf..649fecd54db 100644 --- a/pkg/kubectl/cmd/delete_test.go +++ b/pkg/kubectl/cmd/delete_test.go @@ -35,7 +35,7 @@ func TestDeleteObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "DELETE": + case p == "/namespaces/test/pods/redis-master" && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -63,7 +63,7 @@ func TestDeleteObjectIgnoreNotFound(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "DELETE": + case p == "/namespaces/test/pods/redis-master" && m == "DELETE": return &http.Response{StatusCode: 404, Body: stringBody("")}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -90,7 +90,7 @@ func TestDeleteNoObjects(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods" && m == "GET": + case p == "/namespaces/test/pods" && m == "GET": return &http.Response{StatusCode: 200, Body: objBody(codec, &api.PodList{})}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -123,9 +123,9 @@ func TestDeleteMultipleObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "DELETE": + case p == "/namespaces/test/pods/redis-master" && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/services/frontend" && m == "DELETE": + case p == "/namespaces/test/services/frontend" && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -155,9 +155,9 @@ func TestDeleteMultipleObjectIgnoreMissing(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "DELETE": + case p == "/namespaces/test/pods/redis-master" && m == "DELETE": return &http.Response{StatusCode: 404, Body: stringBody("")}, nil - case p == "/ns/test/services/frontend" && m == "DELETE": + case p == "/namespaces/test/services/frontend" && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -187,11 +187,11 @@ func TestDeleteDirectory(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case strings.HasPrefix(p, "/ns/test/pods/") && m == "DELETE": + case strings.HasPrefix(p, "/namespaces/test/pods/") && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case strings.HasPrefix(p, "/ns/test/services/") && m == "DELETE": + case strings.HasPrefix(p, "/namespaces/test/services/") && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil - case strings.HasPrefix(p, "/ns/test/replicationcontrollers/") && m == "DELETE": + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -220,19 +220,19 @@ func TestDeleteMultipleSelector(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods" && m == "GET": + case p == "/namespaces/test/pods" && m == "GET": if req.URL.Query().Get("labels") != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil - case p == "/ns/test/services" && m == "GET": + case p == "/namespaces/test/services" && m == "GET": if req.URL.Query().Get("labels") != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil - case strings.HasPrefix(p, "/ns/test/pods/") && m == "DELETE": + case strings.HasPrefix(p, "/namespaces/test/pods/") && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case strings.HasPrefix(p, "/ns/test/services/") && m == "DELETE": + case strings.HasPrefix(p, "/namespaces/test/services/") && m == "DELETE": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index 43eebf61331..07f481adb30 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -183,9 +183,9 @@ func TestGetMultipleTypeObjects(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { - case "/ns/test/pods": + case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil - case "/ns/test/services": + case "/namespaces/test/services": return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -219,9 +219,9 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { - case "/ns/test/pods": + case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil - case "/ns/test/services": + case "/namespaces/test/services": return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -271,9 +271,9 @@ func TestGetMultipleTypeObjectsWithSelector(t *testing.T) { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { - case "/ns/test/pods": + case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil - case "/ns/test/services": + case "/namespaces/test/services": return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -359,9 +359,9 @@ func TestWatchSelector(t *testing.T) { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { - case "/ns/test/pods": + case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Body: objBody(codec, &api.PodList{Items: pods})}, nil - case "/watch/ns/test/pods": + case "/watch/namespaces/test/pods": return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -398,9 +398,9 @@ func TestWatchResource(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { - case "/ns/test/pods/foo": + case "/namespaces/test/pods/foo": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods[0])}, nil - case "/watch/ns/test/pods/foo": + case "/watch/namespaces/test/pods/foo": return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -436,9 +436,9 @@ func TestWatchOnlyResource(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { - case "/ns/test/pods/foo": + case "/namespaces/test/pods/foo": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods[0])}, nil - case "/watch/ns/test/pods/foo": + case "/watch/namespaces/test/pods/foo": return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) diff --git a/pkg/kubectl/cmd/update_test.go b/pkg/kubectl/cmd/update_test.go index e17e9458466..9bc9c32dfda 100644 --- a/pkg/kubectl/cmd/update_test.go +++ b/pkg/kubectl/cmd/update_test.go @@ -49,9 +49,9 @@ func TestUpdateObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "GET": + case p == "/namespaces/test/pods/redis-master" && m == "GET": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/pods/redis-master" && m == "PUT": + case p == "/namespaces/test/pods/redis-master" && m == "PUT": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -81,14 +81,14 @@ func TestUpdateMultipleObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/ns/test/pods/redis-master" && m == "GET": + case p == "/namespaces/test/pods/redis-master" && m == "GET": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/pods/redis-master" && m == "PUT": + case p == "/namespaces/test/pods/redis-master" && m == "PUT": return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case p == "/ns/test/services/frontend" && m == "GET": + case p == "/namespaces/test/services/frontend" && m == "GET": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil - case p == "/ns/test/services/frontend" && m == "PUT": + case p == "/namespaces/test/services/frontend" && m == "PUT": return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) @@ -119,11 +119,11 @@ func TestUpdateDirectory(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case strings.HasPrefix(p, "/ns/test/pods/") && (m == "GET" || m == "PUT"): + case strings.HasPrefix(p, "/namespaces/test/pods/") && (m == "GET" || m == "PUT"): return &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, nil - case strings.HasPrefix(p, "/ns/test/services/") && (m == "GET" || m == "PUT"): + case strings.HasPrefix(p, "/namespaces/test/services/") && (m == "GET" || m == "PUT"): return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil - case strings.HasPrefix(p, "/ns/test/replicationcontrollers/") && (m == "GET" || m == "PUT"): + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && (m == "GET" || m == "PUT"): return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) diff --git a/pkg/kubectl/resource/builder_test.go b/pkg/kubectl/resource/builder_test.go index 26f0ed011f0..3405d390540 100644 --- a/pkg/kubectl/resource/builder_test.go +++ b/pkg/kubectl/resource/builder_test.go @@ -304,7 +304,7 @@ func TestURLBuilderRequireNamespace(t *testing.T) { func TestResourceByName(t *testing.T) { pods, _ := testData() b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), + "/namespaces/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), })). NamespaceParam("test") @@ -337,7 +337,7 @@ func TestResourceByName(t *testing.T) { func TestResourceByNameAndEmptySelector(t *testing.T) { pods, _ := testData() b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), + "/namespaces/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), })). NamespaceParam("test"). SelectorParam(""). @@ -364,8 +364,8 @@ func TestResourceByNameAndEmptySelector(t *testing.T) { func TestSelector(t *testing.T) { pods, svc := testData() b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), - "/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), + "/namespaces/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + "/namespaces/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), })). SelectorParam("a=b"). NamespaceParam("test"). @@ -494,7 +494,7 @@ func TestSingularObject(t *testing.T) { func TestListObject(t *testing.T) { pods, _ := testData() b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + "/namespaces/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), })). SelectorParam("a=b"). NamespaceParam("test"). @@ -526,8 +526,8 @@ func TestListObject(t *testing.T) { func TestListObjectWithDifferentVersions(t *testing.T) { pods, svc := testData() obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), - "/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), + "/namespaces/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + "/namespaces/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), })). SelectorParam("a=b"). NamespaceParam("test"). @@ -552,7 +552,7 @@ func TestListObjectWithDifferentVersions(t *testing.T) { func TestWatch(t *testing.T) { pods, _ := testData() w, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/watch/ns/test/pods/redis-master?resourceVersion=10": watchBody(watch.Event{ + "/watch/namespaces/test/pods/redis-master?resourceVersion=10": watchBody(watch.Event{ Type: watch.Added, Object: &pods.Items[0], }), @@ -607,9 +607,9 @@ func TestLatest(t *testing.T) { } b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ - "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, newPod), - "/ns/test/pods/bar": runtime.EncodeOrDie(latest.Codec, newPod2), - "/ns/test/services/baz": runtime.EncodeOrDie(latest.Codec, newSvc), + "/namespaces/test/pods/foo": runtime.EncodeOrDie(latest.Codec, newPod), + "/namespaces/test/pods/bar": runtime.EncodeOrDie(latest.Codec, newPod2), + "/namespaces/test/services/baz": runtime.EncodeOrDie(latest.Codec, newSvc), })). NamespaceParam("other").Stream(r, "STDIN").Flatten().Latest() diff --git a/pkg/kubectl/resource/helper_test.go b/pkg/kubectl/resource/helper_test.go index f4d752a1da2..e133efae98c 100644 --- a/pkg/kubectl/resource/helper_test.go +++ b/pkg/kubectl/resource/helper_test.go @@ -318,7 +318,7 @@ func TestHelperList(t *testing.T) { t.Errorf("unexpected method: %#v", req) return false } - if req.URL.Path != "/ns/bar" { + if req.URL.Path != "/namespaces/bar" { t.Errorf("url doesn't contain name: %#v", req.URL) return false } diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index efd85b62bb3..3e86778de05 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -224,6 +224,7 @@ var statusColumns = []string{"STATUS"} var eventColumns = []string{"TIME", "NAME", "KIND", "SUBOBJECT", "REASON", "SOURCE", "MESSAGE"} var limitRangeColumns = []string{"NAME"} var resourceQuotaColumns = []string{"NAME"} +var namespaceColumns = []string{"NAME", "LABELS"} // addDefaultHandlers adds print handlers for default Kubernetes types. func (h *HumanReadablePrinter) addDefaultHandlers() { @@ -243,6 +244,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(limitRangeColumns, printLimitRangeList) h.Handler(resourceQuotaColumns, printResourceQuota) h.Handler(resourceQuotaColumns, printResourceQuotaList) + h.Handler(namespaceColumns, printNamespace) + h.Handler(namespaceColumns, printNamespaceList) } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { @@ -366,6 +369,20 @@ func printEndpoints(endpoint *api.Endpoints, w io.Writer) error { return err } +func printNamespace(item *api.Namespace, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%s\n", item.Name, formatLabels(item.Labels)) + return err +} + +func printNamespaceList(list *api.NamespaceList, w io.Writer) error { + for _, item := range list.Items { + if err := printNamespace(&item, w); err != nil { + return err + } + } + return nil +} + func printMinion(minion *api.Node, w io.Writer) error { conditionMap := make(map[api.NodeConditionKind]*api.NodeCondition) NodeAllConditions := []api.NodeConditionKind{api.NodeReady, api.NodeReachable} diff --git a/pkg/master/master.go b/pkg/master/master.go index 02c39c5143f..c86ef744b10 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -50,6 +50,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/limitrange" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage" @@ -122,6 +123,7 @@ type Master struct { eventRegistry generic.Registry limitRangeRegistry generic.Registry resourceQuotaRegistry resourcequota.Registry + namespaceRegistry generic.Registry storage map[string]apiserver.RESTStorage client *client.Client portalNet *net.IPNet @@ -279,6 +281,7 @@ func New(c *Config) *Master { endpointRegistry: etcd.NewRegistry(c.EtcdHelper, nil), bindingRegistry: etcd.NewRegistry(c.EtcdHelper, boundPodFactory), eventRegistry: event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())), + namespaceRegistry: namespace.NewEtcdRegistry(c.EtcdHelper), minionRegistry: minionRegistry, limitRangeRegistry: limitrange.NewEtcdRegistry(c.EtcdHelper), resourceQuotaRegistry: resourcequota.NewEtcdRegistry(c.EtcdHelper), @@ -400,6 +403,7 @@ func (m *Master) init(c *Config) { "limitRanges": limitrange.NewREST(m.limitRangeRegistry), "resourceQuotas": resourcequota.NewREST(m.resourceQuotaRegistry), "resourceQuotaUsages": resourcequotausage.NewREST(m.resourceQuotaRegistry), + "namespaces": namespace.NewREST(m.namespaceRegistry), } apiVersions := []string{"v1beta1", "v1beta2"} diff --git a/pkg/master/publish.go b/pkg/master/publish.go index 54710b19d9e..47b9803f478 100644 --- a/pkg/master/publish.go +++ b/pkg/master/publish.go @@ -34,6 +34,9 @@ func (m *Master) serviceWriterLoop(stop chan struct{}) { // TODO: when it becomes possible to change this stuff, // stop polling and start watching. // TODO: add endpoints of all replicas, not just the elected master. + if err := m.createMasterNamespaceIfNeeded(api.NamespaceDefault); err != nil { + glog.Errorf("Can't create master namespace: %v", err) + } if m.serviceReadWriteIP != nil { if err := m.createMasterServiceIfNeeded("kubernetes", m.serviceReadWriteIP, m.serviceReadWritePort); err != nil { glog.Errorf("Can't create rw service: %v", err) @@ -56,6 +59,9 @@ func (m *Master) roServiceWriterLoop(stop chan struct{}) { // Update service & endpoint records. // TODO: when it becomes possible to change this stuff, // stop polling and start watching. + if err := m.createMasterNamespaceIfNeeded(api.NamespaceDefault); err != nil { + glog.Errorf("Can't create master namespace: %v", err) + } if m.serviceReadOnlyIP != nil { if err := m.createMasterServiceIfNeeded("kubernetes-ro", m.serviceReadOnlyIP, m.serviceReadOnlyPort); err != nil { glog.Errorf("Can't create ro service: %v", err) @@ -73,6 +79,30 @@ func (m *Master) roServiceWriterLoop(stop chan struct{}) { } } +// createMasterNamespaceIfNeeded will create the namespace that contains the master services if it doesn't already exist +func (m *Master) createMasterNamespaceIfNeeded(ns string) error { + ctx := api.NewContext() + if _, err := m.namespaceRegistry.Get(ctx, api.NamespaceDefault); err == nil { + // the namespace already exists + return nil + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: ns, + Namespace: "", + }, + } + c, err := m.storage["namespaces"].(apiserver.RESTCreater).Create(ctx, namespace) + if err != nil { + return err + } + resp := <-c + if _, ok := resp.Object.(*api.Service); ok { + return nil + } + return fmt.Errorf("unexpected response %#v", resp) +} + // createMasterServiceIfNeeded will create the specified service if it // doesn't already exist. func (m *Master) createMasterServiceIfNeeded(serviceName string, serviceIP net.IP, servicePort int) error { diff --git a/pkg/master/server/plugins.go b/pkg/master/server/plugins.go index 472adbe9246..9f3039b7c0c 100644 --- a/pkg/master/server/plugins.go +++ b/pkg/master/server/plugins.go @@ -31,6 +31,8 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" ) diff --git a/pkg/registry/event/rest.go b/pkg/registry/event/rest.go index b736c466beb..216c356d032 100644 --- a/pkg/registry/event/rest.go +++ b/pkg/registry/event/rest.go @@ -47,7 +47,7 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan apiserver.RE if !ok { return nil, fmt.Errorf("invalid object type") } - if api.Namespace(ctx) != "" { + if api.NamespaceValue(ctx) != "" { if !api.ValidNamespace(ctx, &event.ObjectMeta) { return nil, errors.NewConflict("event", event.Namespace, fmt.Errorf("event.namespace does not match the provided context")) } @@ -72,7 +72,7 @@ func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RE if !ok { return nil, fmt.Errorf("not an event object: %#v", obj) } - if api.Namespace(ctx) != "" { + if api.NamespaceValue(ctx) != "" { if !api.ValidNamespace(ctx, &event.ObjectMeta) { return nil, errors.NewConflict("event", event.Namespace, fmt.Errorf("event.namespace does not match the provided context")) } diff --git a/pkg/registry/event/rest_test.go b/pkg/registry/event/rest_test.go index f59feb993ae..922603dd9b0 100644 --- a/pkg/registry/event/rest_test.go +++ b/pkg/registry/event/rest_test.go @@ -77,7 +77,7 @@ func TestRESTCreate(t *testing.T) { c, err := rest.Create(item.ctx, item.event) if !item.valid { if err == nil { - ctxNS := api.Namespace(item.ctx) + ctxNS := api.NamespaceValue(item.ctx) t.Errorf("unexpected non-error for %v (%v, %v)", item.event.Name, ctxNS, item.event.Namespace) } continue diff --git a/pkg/registry/namespace/doc.go b/pkg/registry/namespace/doc.go new file mode 100644 index 00000000000..533e38f5ba7 --- /dev/null +++ b/pkg/registry/namespace/doc.go @@ -0,0 +1,19 @@ +/* +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 namespace provides Registry interface and it's REST +// implementation for storing Namespace api objects. +package namespace diff --git a/pkg/registry/namespace/registry.go b/pkg/registry/namespace/registry.go new file mode 100644 index 00000000000..5c6186834f4 --- /dev/null +++ b/pkg/registry/namespace/registry.go @@ -0,0 +1,48 @@ +/* +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 namespace + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// registry implements custom changes to generic.Etcd for Namespace storage +type registry struct { + *etcdgeneric.Etcd +} + +// NewEtcdRegistry returns a registry which will store Namespace objects in the given EtcdHelper. +func NewEtcdRegistry(h tools.EtcdHelper) generic.Registry { + return registry{ + Etcd: &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.Namespace{} }, + NewListFunc: func() runtime.Object { return &api.NamespaceList{} }, + EndpointName: "namespaces", + KeyRootFunc: func(ctx api.Context) string { + return "/registry/namespaces" + }, + KeyFunc: func(ctx api.Context, id string) (string, error) { + return "/registry/namespaces/" + id, nil + }, + Helper: h, + }, + } +} diff --git a/pkg/registry/namespace/registry_test.go b/pkg/registry/namespace/registry_test.go new file mode 100644 index 00000000000..3c7732862f5 --- /dev/null +++ b/pkg/registry/namespace/registry_test.go @@ -0,0 +1,17 @@ +/* +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 namespace diff --git a/pkg/registry/namespace/rest.go b/pkg/registry/namespace/rest.go new file mode 100644 index 00000000000..eeaad26519d --- /dev/null +++ b/pkg/registry/namespace/rest.go @@ -0,0 +1,134 @@ +/* +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 namespace + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + 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/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// REST provides the RESTStorage access patterns to work with Namespace objects. +type REST struct { + registry generic.Registry +} + +// NewREST returns a new REST. You must use a registry created by +// NewEtcdRegistry unless you're testing. +func NewREST(registry generic.Registry) *REST { + return &REST{ + registry: registry, + } +} + +// Create creates a Namespace object +func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + namespace := obj.(*api.Namespace) + if err := rest.BeforeCreate(rest.Namespaces, ctx, obj); err != nil { + return nil, err + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + if err := rs.registry.Create(ctx, namespace.Name, namespace); err != nil { + err = rest.CheckGeneratedNameError(rest.Namespaces, err, namespace) + return nil, err + } + return rs.registry.Get(ctx, namespace.Name) + }), nil +} + +// Update updates a Namespace object. +func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + namespace, ok := obj.(*api.Namespace) + if !ok { + return nil, fmt.Errorf("not a namespace: %#v", obj) + } + + oldObj, err := rs.registry.Get(ctx, namespace.Name) + if err != nil { + return nil, err + } + + oldNamespace := oldObj.(*api.Namespace) + if errs := validation.ValidateNamespaceUpdate(oldNamespace, namespace); len(errs) > 0 { + return nil, kerrors.NewInvalid("namespace", namespace.Name, errs) + } + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Update(ctx, oldNamespace.Name, oldNamespace) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, oldNamespace.Name) + }), nil +} + +// Delete deletes the Namespace with the specified name +func (rs *REST) Delete(ctx api.Context, id string) (<-chan apiserver.RESTResult, error) { + obj, err := rs.registry.Get(ctx, id) + if err != nil { + return nil, err + } + _, ok := obj.(*api.Namespace) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + + return apiserver.MakeAsync(func() (runtime.Object, error) { + return &api.Status{Status: api.StatusSuccess}, rs.registry.Delete(ctx, id) + }), nil +} + +func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) { + obj, err := rs.registry.Get(ctx, id) + if err != nil { + return nil, err + } + namespace, ok := obj.(*api.Namespace) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + return namespace, err +} + +func (rs *REST) getAttrs(obj runtime.Object) (objLabels, objFields labels.Set, err error) { + return labels.Set{}, labels.Set{}, nil +} + +func (rs *REST) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { + return rs.registry.List(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}) +} + +func (rs *REST) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) { + return rs.registry.Watch(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}, resourceVersion) +} + +// New returns a new api.Namespace +func (*REST) New() runtime.Object { + return &api.Namespace{} +} + +func (*REST) NewList() runtime.Object { + return &api.NamespaceList{} +} diff --git a/pkg/registry/namespace/rest_test.go b/pkg/registry/namespace/rest_test.go new file mode 100644 index 00000000000..e053e4dd215 --- /dev/null +++ b/pkg/registry/namespace/rest_test.go @@ -0,0 +1,188 @@ +/* +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 namespace + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type testRegistry struct { + *registrytest.GenericRegistry +} + +func NewTestREST() (testRegistry, *REST) { + reg := testRegistry{registrytest.NewGeneric(nil)} + return reg, NewREST(reg) +} + +func testNamespace(name string) *api.Namespace { + return &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: name, + }, + } +} + +func TestRESTCreate(t *testing.T) { + table := []struct { + ctx api.Context + namespace *api.Namespace + valid bool + }{ + { + ctx: api.NewContext(), + namespace: testNamespace("foo"), + valid: true, + }, { + ctx: api.NewContext(), + namespace: testNamespace("bar"), + valid: true, + }, + } + + for _, item := range table { + _, rest := NewTestREST() + c, err := rest.Create(item.ctx, item.namespace) + if !item.valid { + if err == nil { + t.Errorf("unexpected non-error for %v", item.namespace.Name) + } + continue + } + if err != nil { + t.Errorf("%v: Unexpected error %v", item.namespace.Name, err) + continue + } + if !api.HasObjectMetaSystemFieldValues(&item.namespace.ObjectMeta) { + t.Errorf("storage did not populate object meta field values") + } + if e, a := item.namespace, (<-c).Object; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } + // Ensure we implement the interface + _ = apiserver.ResourceWatcher(rest) + } +} + +func TestRESTUpdate(t *testing.T) { + _, rest := NewTestREST() + namespaceA := testNamespace("foo") + c, err := rest.Create(api.NewDefaultContext(), namespaceA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + got, err := rest.Get(api.NewDefaultContext(), namespaceA.Name) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := namespaceA, got; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } + namespaceB := testNamespace("foo") + u, err := rest.Update(api.NewDefaultContext(), namespaceB) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-u + got2, err := rest.Get(api.NewDefaultContext(), namespaceB.Name) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := namespaceB, got2; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } + +} + +func TestRESTDelete(t *testing.T) { + _, rest := NewTestREST() + namespaceA := testNamespace("foo") + c, err := rest.Create(api.NewContext(), namespaceA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + c, err = rest.Delete(api.NewContext(), namespaceA.Name) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if stat := (<-c).Object.(*api.Status); stat.Status != api.StatusSuccess { + t.Errorf("unexpected status: %v", stat) + } +} + +func TestRESTGet(t *testing.T) { + _, rest := NewTestREST() + namespaceA := testNamespace("foo") + c, err := rest.Create(api.NewContext(), namespaceA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + got, err := rest.Get(api.NewContext(), namespaceA.Name) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := namespaceA, got; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } +} + +func TestRESTList(t *testing.T) { + reg, rest := NewTestREST() + namespaceA := testNamespace("foo") + namespaceB := testNamespace("bar") + namespaceC := testNamespace("baz") + reg.ObjectList = &api.NamespaceList{ + Items: []api.Namespace{*namespaceA, *namespaceB, *namespaceC}, + } + got, err := rest.List(api.NewContext(), labels.Everything(), labels.Everything()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + expect := &api.NamespaceList{ + Items: []api.Namespace{*namespaceA, *namespaceB, *namespaceC}, + } + if e, a := expect, got; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } +} + +func TestRESTWatch(t *testing.T) { + namespaceA := testNamespace("foo") + reg, rest := NewTestREST() + wi, err := rest.Watch(api.NewContext(), labels.Everything(), labels.Everything(), "0") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + go func() { + reg.Broadcaster.Action(watch.Added, namespaceA) + }() + got := <-wi.ResultChan() + if e, a := namespaceA, got.Object; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", util.ObjectDiff(e, a)) + } +} diff --git a/plugin/pkg/admission/namespace/autoprovision/admission.go b/plugin/pkg/admission/namespace/autoprovision/admission.go new file mode 100644 index 00000000000..42f724b6d98 --- /dev/null +++ b/plugin/pkg/admission/namespace/autoprovision/admission.go @@ -0,0 +1,94 @@ +/* +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 autoprovision + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +func init() { + admission.RegisterPlugin("NamespaceAutoProvision", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewProvision(client), nil + }) +} + +// provision is an implementation of admission.Interface. +// It looks at all incoming requests in a namespace context, and if the namespace does not exist, it creates one. +// It is useful in deployments that do not want to restrict creation of a namespace prior to its usage. +type provision struct { + client client.Interface + store cache.Store +} + +func (p *provision) Admit(a admission.Attributes) (err error) { + defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) + if err != nil { + return err + } + mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) + if err != nil { + return err + } + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + return nil + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := p.store.Get(namespace) + if err != nil { + return err + } + if exists { + return nil + } + _, err = p.client.Namespaces().Create(namespace) + if err != nil { + return err + } + return nil +} + +func NewProvision(c client.Interface) admission.Interface { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + reflector := cache.NewReflector( + &cache.ListWatch{ + Client: c.(*client.Client), + FieldSelector: labels.Everything(), + Resource: "namespaces", + }, + &api.Namespace{}, + store, + ) + reflector.Run() + return &provision{ + client: c, + store: store, + } +} diff --git a/plugin/pkg/admission/namespace/autoprovision/admission_test.go b/plugin/pkg/admission/namespace/autoprovision/admission_test.go new file mode 100644 index 00000000000..8c95eac68e5 --- /dev/null +++ b/plugin/pkg/admission/namespace/autoprovision/admission_test.go @@ -0,0 +1,17 @@ +/* +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 autoprovision diff --git a/plugin/pkg/admission/namespace/exists/admission.go b/plugin/pkg/admission/namespace/exists/admission.go new file mode 100644 index 00000000000..453b0860cd3 --- /dev/null +++ b/plugin/pkg/admission/namespace/exists/admission.go @@ -0,0 +1,98 @@ +/* +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 exists + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +func init() { + admission.RegisterPlugin("NamespaceExists", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewExists(client), nil + }) +} + +// exists is an implementation of admission.Interface. +// It rejects all incoming requests in a namespace context if the namespace does not exist. +// It is useful in deployments that want to enforce pre-declaration of a Namespace resource. +type exists struct { + client client.Interface + store cache.Store +} + +func (e *exists) Admit(a admission.Attributes) (err error) { + defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) + if err != nil { + return err + } + mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) + if err != nil { + return err + } + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + return nil + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := e.store.Get(namespace) + if err != nil { + return err + } + if exists { + return nil + } + obj := a.GetObject() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s does not exist", a.GetNamespace())) +} + +func NewExists(c client.Interface) admission.Interface { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + // TODO: look into a list/watch that can work with client.Interface, maybe pass it a ListFunc and a WatchFunc + reflector := cache.NewReflector( + &cache.ListWatch{ + Client: c.(*client.Client), + FieldSelector: labels.Everything(), + Resource: "namespaces", + }, + &api.Namespace{}, + store, + ) + reflector.Run() + return &exists{ + client: c, + store: store, + } +} diff --git a/plugin/pkg/admission/namespace/exists/admission_test.go b/plugin/pkg/admission/namespace/exists/admission_test.go new file mode 100644 index 00000000000..b1d367a3e05 --- /dev/null +++ b/plugin/pkg/admission/namespace/exists/admission_test.go @@ -0,0 +1,17 @@ +/* +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 exists diff --git a/plugin/pkg/scheduler/factory/factory.go b/plugin/pkg/scheduler/factory/factory.go index 02e3680a2f3..155dcbb7b10 100644 --- a/plugin/pkg/scheduler/factory/factory.go +++ b/plugin/pkg/scheduler/factory/factory.go @@ -273,7 +273,7 @@ type binder struct { func (b *binder) Bind(binding *api.Binding) error { glog.V(2).Infof("Attempting to bind %v to %v", binding.PodID, binding.Host) ctx := api.WithNamespace(api.NewContext(), binding.Namespace) - return b.Post().Namespace(api.Namespace(ctx)).Resource("bindings").Body(binding).Do().Error() + return b.Post().Namespace(api.NamespaceValue(ctx)).Resource("bindings").Body(binding).Do().Error() } type clock interface {