diff --git a/pkg/apis/extensions/register.go b/pkg/apis/extensions/register.go index b346c89dbdb..52b4755fa99 100644 --- a/pkg/apis/extensions/register.go +++ b/pkg/apis/extensions/register.go @@ -65,6 +65,8 @@ func addKnownTypes() { &Ingress{}, &IngressList{}, &api.ListOptions{}, + &ConfigMap{}, + &ConfigMapList{}, ) } @@ -86,3 +88,5 @@ func (obj *ThirdPartyResourceData) GetObjectKind() unversioned.ObjectKind { func (obj *ThirdPartyResourceDataList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *Ingress) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *IngressList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ConfigMap) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ConfigMapList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 147c47ffba0..707899867aa 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -691,3 +691,26 @@ const ( LabelSelectorOpExists LabelSelectorOperator = "Exists" LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist" ) + +// ConfigMap holds configuration data for components or applications to consume. +type ConfigMap struct { + unversioned.TypeMeta `json:",inline"` + + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + api.ObjectMeta `json:"metadata,omitempty"` + + // Data contains the configuration data. + // Each key must be a valid DNS_SUBDOMAIN with an optional leading dot. + Data map[string]string `json:"data,omitempty"` +} + +// ConfigMapList is a resource containing a list of ConfigMap objects. +type ConfigMapList struct { + unversioned.TypeMeta `json:",inline"` + + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + unversioned.ListMeta `json:"metadata,omitempty"` + + // Items is the list of ConfigMaps. + Items []ConfigMap `json:"items,omitempty"` +} diff --git a/pkg/apis/extensions/v1beta1/defaults.go b/pkg/apis/extensions/v1beta1/defaults.go index edb504e1b8d..3344df1217b 100644 --- a/pkg/apis/extensions/v1beta1/defaults.go +++ b/pkg/apis/extensions/v1beta1/defaults.go @@ -118,5 +118,10 @@ func addDefaultingFuncs() { obj.Spec.CPUUtilization = &CPUTargetUtilization{TargetPercentage: 80} } }, + func(obj *ConfigMap) { + if obj.Data == nil { + obj.Data = make(map[string]string) + } + }, ) } diff --git a/pkg/apis/extensions/v1beta1/register.go b/pkg/apis/extensions/v1beta1/register.go index 7105405e18f..4eb7a7f06bb 100644 --- a/pkg/apis/extensions/v1beta1/register.go +++ b/pkg/apis/extensions/v1beta1/register.go @@ -58,6 +58,8 @@ func addKnownTypes() { &Ingress{}, &IngressList{}, &ListOptions{}, + &ConfigMap{}, + &ConfigMapList{}, ) } @@ -80,3 +82,5 @@ func (obj *ThirdPartyResourceDataList) GetObjectKind() unversioned.ObjectKind { func (obj *Ingress) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *IngressList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *ListOptions) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ConfigMap) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *ConfigMapList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index d41c252679b..57b12f60f23 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -719,3 +719,26 @@ const ( LabelSelectorOpExists LabelSelectorOperator = "Exists" LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist" ) + +// ConfigMap holds configuration data for pods to consume. +type ConfigMap struct { + unversioned.TypeMeta `json:",inline"` + + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + v1.ObjectMeta `json:"metadata,omitempty"` + + // Data contains the configuration data. + // Each key must be a valid DNS_SUBDOMAIN with an optional leading dot. + Data map[string]string `json:"data,omitempty"` +} + +// ConfigMapList is a resource containing a list of ConfigMap objects. +type ConfigMapList struct { + unversioned.TypeMeta `json:",inline"` + + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + unversioned.ListMeta `json:"metadata,omitempty"` + + // Items is the list of ConfigMaps. + Items []ConfigMap `json:"items,omitempty"` +} diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index ac347500549..f0719207c43 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "fmt" "net" "regexp" "strconv" @@ -593,3 +594,33 @@ func ValidateScale(scale *extensions.Scale) field.ErrorList { return allErrs } + +// ValidateConfigMapName can be used to check whether the given ConfigMap name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +func ValidateConfigMapName(name string, prefix bool) (bool, string) { + return apivalidation.NameIsDNSSubdomain(name, prefix) +} + +// ValidateConfigMap tests whether required fields in the ConfigMap are set. +func ValidateConfigMap(cfg *extensions.ConfigMap) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&cfg.ObjectMeta, true, ValidateConfigMapName, field.NewPath("metadata"))...) + + for key := range cfg.Data { + if !apivalidation.IsSecretKey(key) { + allErrs = append(allErrs, field.Invalid(field.NewPath("data").Key(key), key, fmt.Sprintf("must have at most %d characters and match regex %s", validation.DNS1123SubdomainMaxLength, apivalidation.SecretKeyFmt))) + } + } + + return allErrs +} + +// ValidateConfigMapUpdate tests if required fields in the ConfigMap are set. +func ValidateConfigMapUpdate(newCfg, oldCfg *extensions.ConfigMap) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newCfg.ObjectMeta, &oldCfg.ObjectMeta, field.NewPath("metadata"))...) + allErrs = append(allErrs, ValidateConfigMap(newCfg)...) + + return allErrs +} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index eedf591ebff..827cb4d3cb4 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -1300,3 +1300,105 @@ func newInt(val int) *int { *p = val return p } + +func TestValidateConfigMap(t *testing.T) { + newConfigMap := func(name, namespace string, data map[string]string) extensions.ConfigMap { + return extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } + } + + var ( + validConfigMap = newConfigMap("validname", "validns", map[string]string{"key": "value"}) + maxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 253): "value"}) + + emptyName = newConfigMap("", "validns", nil) + invalidName = newConfigMap("NoUppercaseOrSpecialCharsLike=Equals", "validns", nil) + emptyNs = newConfigMap("validname", "", nil) + invalidNs = newConfigMap("validname", "NoUppercaseOrSpecialCharsLike=Equals", nil) + invalidKey = newConfigMap("validname", "validns", map[string]string{"a..b": "value"}) + leadingDotKey = newConfigMap("validname", "validns", map[string]string{".ab": "value"}) + dotKey = newConfigMap("validname", "validns", map[string]string{".": "value"}) + doubleDotKey = newConfigMap("validname", "validns", map[string]string{"..": "value"}) + overMaxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 254): "value"}) + ) + + tests := map[string]struct { + cfg extensions.ConfigMap + isValid bool + }{ + "valid": {validConfigMap, true}, + "max key length": {maxKeyLength, true}, + "leading dot key": {leadingDotKey, true}, + "empty name": {emptyName, false}, + "invalid name": {invalidName, false}, + "invalid key": {invalidKey, false}, + "empty namespace": {emptyNs, false}, + "invalid namespace": {invalidNs, false}, + "dot key": {dotKey, false}, + "double dot key": {doubleDotKey, false}, + "over max key length": {overMaxKeyLength, false}, + } + + for name, tc := range tests { + errs := ValidateConfigMap(&tc.cfg) + if tc.isValid && len(errs) > 0 { + t.Errorf("%v: unexpected error: %v", name, errs) + } + if !tc.isValid && len(errs) == 0 { + t.Errorf("%v: unexpected non-error", name) + } + } +} + +func TestValidateConfigMapUpdate(t *testing.T) { + newConfigMap := func(version, name, namespace string, data map[string]string) extensions.ConfigMap { + return extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: namespace, + ResourceVersion: version, + }, + Data: data, + } + } + + var ( + validConfigMap = newConfigMap("1", "validname", "validns", map[string]string{"key": "value"}) + noVersion = newConfigMap("", "validname", "validns", map[string]string{"key": "value"}) + ) + + cases := []struct { + name string + newCfg extensions.ConfigMap + oldCfg extensions.ConfigMap + isValid bool + }{ + { + name: "valid", + newCfg: validConfigMap, + oldCfg: validConfigMap, + isValid: true, + }, + { + name: "invalid", + newCfg: noVersion, + oldCfg: validConfigMap, + isValid: false, + }, + } + + for _, tc := range cases { + errs := ValidateConfigMapUpdate(&tc.newCfg, &tc.oldCfg) + if tc.isValid && len(errs) > 0 { + t.Errorf("%v: unexpected error: %v", tc.name, errs) + } + if !tc.isValid && len(errs) == 0 { + t.Errorf("%v: unexpected non-error", tc.name) + } + } +} diff --git a/pkg/client/unversioned/configmap.go b/pkg/client/unversioned/configmap.go new file mode 100644 index 00000000000..0c41a4f4ac1 --- /dev/null +++ b/pkg/client/unversioned/configmap.go @@ -0,0 +1,123 @@ +/* +Copyright 2015 The Kubernetes Authors 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 unversioned + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/watch" +) + +const ( + ConfigMapResourceName string = "configmaps" +) + +type ConfigMapsNamespacer interface { + ConfigMaps(namespace string) ConfigMapsInterface +} + +type ConfigMapsInterface interface { + Get(string) (*extensions.ConfigMap, error) + List(opts api.ListOptions) (*extensions.ConfigMapList, error) + Create(*extensions.ConfigMap) (*extensions.ConfigMap, error) + Delete(string) error + Update(*extensions.ConfigMap) (*extensions.ConfigMap, error) + Watch(api.ListOptions) (watch.Interface, error) +} + +type ConfigMaps struct { + client *ExtensionsClient + namespace string +} + +// ConfigMaps should implement ConfigMapsInterface +var _ ConfigMapsInterface = &ConfigMaps{} + +func newConfigMaps(c *ExtensionsClient, ns string) *ConfigMaps { + return &ConfigMaps{ + client: c, + namespace: ns, + } +} + +func (c *ConfigMaps) Get(name string) (*extensions.ConfigMap, error) { + result := &extensions.ConfigMap{} + err := c.client.Get(). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + Name(name). + Do(). + Into(result) + + return result, err +} + +func (c *ConfigMaps) List(opts api.ListOptions) (*extensions.ConfigMapList, error) { + result := &extensions.ConfigMapList{} + err := c.client.Get(). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + VersionedParams(&opts, api.Scheme). + Do(). + Into(result) + + return result, err +} + +func (c *ConfigMaps) Create(cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + result := &extensions.ConfigMap{} + err := c.client.Post(). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + Body(cfg). + Do(). + Into(result) + + return result, err +} + +func (c *ConfigMaps) Delete(name string) error { + return c.client.Delete(). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + Name(name). + Do(). + Error() +} + +func (c *ConfigMaps) Update(cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + result := &extensions.ConfigMap{} + + err := c.client.Put(). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + Name(cfg.Name). + Body(cfg). + Do(). + Into(result) + + return result, err +} + +func (c *ConfigMaps) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.client.Get(). + Prefix("watch"). + Namespace(c.namespace). + Resource(ConfigMapResourceName). + VersionedParams(&opts, api.Scheme). + Watch() +} diff --git a/pkg/client/unversioned/extensions.go b/pkg/client/unversioned/extensions.go index 3723dce773e..093ebb5fe5e 100644 --- a/pkg/client/unversioned/extensions.go +++ b/pkg/client/unversioned/extensions.go @@ -39,6 +39,7 @@ type ExtensionsInterface interface { JobsNamespacer IngressNamespacer ThirdPartyResourceNamespacer + ConfigMapsNamespacer } // ExtensionsClient is used to interact with experimental Kubernetes features. @@ -101,6 +102,10 @@ func (c *ExtensionsClient) Ingress(namespace string) IngressInterface { return newIngress(c, namespace) } +func (c *ExtensionsClient) ConfigMaps(namespace string) ConfigMapsInterface { + return newConfigMaps(c, namespace) +} + func (c *ExtensionsClient) ThirdPartyResources(namespace string) ThirdPartyResourceInterface { return newThirdPartyResources(c, namespace) } diff --git a/pkg/client/unversioned/testclient/fake_configmaps.go b/pkg/client/unversioned/testclient/fake_configmaps.go new file mode 100644 index 00000000000..8d1dd7f5308 --- /dev/null +++ b/pkg/client/unversioned/testclient/fake_configmaps.go @@ -0,0 +1,79 @@ +/* +Copyright 2015 The Kubernetes Authors 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 testclient + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/watch" +) + +const ( + configMapResourceName string = "configMaps" +) + +// FakeConfigMaps implements ConfigMapInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the method you want to test easier. +type FakeConfigMaps struct { + Fake *FakeExperimental + Namespace string +} + +func (c *FakeConfigMaps) Get(name string) (*extensions.ConfigMap, error) { + obj, err := c.Fake.Invokes(NewGetAction(configMapResourceName, c.Namespace, name), &extensions.ConfigMap{}) + if obj == nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), err +} + +func (c *FakeConfigMaps) List(opts api.ListOptions) (*extensions.ConfigMapList, error) { + obj, err := c.Fake.Invokes(NewListAction(configMapResourceName, c.Namespace, opts), &extensions.ConfigMapList{}) + if obj == nil { + return nil, err + } + + return obj.(*extensions.ConfigMapList), err +} + +func (c *FakeConfigMaps) Create(cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + obj, err := c.Fake.Invokes(NewCreateAction(configMapResourceName, c.Namespace, cfg), cfg) + if obj == nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), err +} + +func (c *FakeConfigMaps) Update(cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + obj, err := c.Fake.Invokes(NewUpdateAction(configMapResourceName, c.Namespace, cfg), cfg) + if obj == nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), err +} + +func (c *FakeConfigMaps) Delete(name string) error { + _, err := c.Fake.Invokes(NewDeleteAction(configMapResourceName, c.Namespace, name), &extensions.ConfigMap{}) + return err +} + +func (c *FakeConfigMaps) Watch(opts api.ListOptions) (watch.Interface, error) { + return c.Fake.InvokesWatch(NewWatchAction(configMapResourceName, c.Namespace, opts)) +} diff --git a/pkg/client/unversioned/testclient/testclient.go b/pkg/client/unversioned/testclient/testclient.go index ebbc6008efd..c7d817eaf45 100644 --- a/pkg/client/unversioned/testclient/testclient.go +++ b/pkg/client/unversioned/testclient/testclient.go @@ -353,6 +353,10 @@ func (c *FakeExperimental) Ingress(namespace string) client.IngressInterface { return &FakeIngress{Fake: c, Namespace: namespace} } +func (c *FakeExperimental) ConfigMaps(namespace string) client.ConfigMapsInterface { + return &FakeConfigMaps{Fake: c, Namespace: namespace} +} + func (c *FakeExperimental) ThirdPartyResources(namespace string) client.ThirdPartyResourceInterface { return &FakeThirdPartyResources{Fake: c, Namespace: namespace} } diff --git a/pkg/registry/configmap/doc.go b/pkg/registry/configmap/doc.go new file mode 100644 index 00000000000..ec8cc087d83 --- /dev/null +++ b/pkg/registry/configmap/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap provides Registry interface +// and its REST implementation for storing +// ConfigMap API objects. +package configmap diff --git a/pkg/registry/configmap/etcd/etcd.go b/pkg/registry/configmap/etcd/etcd.go new file mode 100644 index 00000000000..d92e4007c0f --- /dev/null +++ b/pkg/registry/configmap/etcd/etcd.go @@ -0,0 +1,79 @@ +/* +Copyright 2015 The Kubernetes Authors 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 etcd + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/registry/configmap" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" + + etcdgeneric "k8s.io/kubernetes/pkg/registry/generic/etcd" +) + +// REST implements a RESTStorage for ConfigMap against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work with ConfigMap objects. +func NewREST(s storage.Interface, storageDecorator generic.StorageDecorator) *REST { + prefix := "/configmaps" + + newListFunc := func() runtime.Object { return &extensions.ConfigMapList{} } + storageInterface := storageDecorator( + s, 100, &extensions.ConfigMap{}, prefix, false, newListFunc) + + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { + return &extensions.ConfigMap{} + }, + + // NewListFunc returns an object to store results of an etcd list. + NewListFunc: newListFunc, + + // Produces a path that etcd understands, to the root of the resource + // by combining the namespace in the context with the given prefix. + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + + // Produces a path that etcd understands, to the resource by combining + // the namespace in the context with the given prefix + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name) + }, + + // Retrieves the name field of a ConfigMap object. + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*extensions.ConfigMap).Name, nil + }, + + // Matches objects based on labels/fields for list and watch + PredicateFunc: configmap.MatchConfigMap, + + EndpointName: "configmaps", + + CreateStrategy: configmap.Strategy, + UpdateStrategy: configmap.Strategy, + + Storage: storageInterface, + } + return &REST{store} +} diff --git a/pkg/registry/configmap/etcd/etcd_test.go b/pkg/registry/configmap/etcd/etcd_test.go new file mode 100644 index 00000000000..1bf619e3df3 --- /dev/null +++ b/pkg/registry/configmap/etcd/etcd_test.go @@ -0,0 +1,149 @@ +/* +Copyright 2015 The Kubernetes Authors 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 etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/registry/registrytest" + "k8s.io/kubernetes/pkg/runtime" + etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing" +) + +func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, "extensions") + return NewREST(etcdStorage, generic.UndecoratedStorage), server +} + +func validNewConfigMap() *extensions.ConfigMap { + return &extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "default", + Labels: map[string]string{ + "label-1": "value-1", + "label-2": "value-2", + }, + }, + Data: map[string]string{ + "test": "data", + }, + } +} + +func TestCreate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + + validConfigMap := validNewConfigMap() + validConfigMap.ObjectMeta = api.ObjectMeta{ + GenerateName: "foo-", + } + + test.TestCreate( + validConfigMap, + &extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{Name: "badName"}, + Data: map[string]string{ + "key": "value", + }, + }, + &extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{Name: "name-2"}, + Data: map[string]string{ + "..dotfile": "do: nothing\n", + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + test.TestUpdate( + // valid + validNewConfigMap(), + // updateFunc + func(obj runtime.Object) runtime.Object { + cfg := obj.(*extensions.ConfigMap) + cfg.Data["update-test"] = "value" + return cfg + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + cfg := obj.(*extensions.ConfigMap) + cfg.Data["badKey"] = "value" + return cfg + }, + ) +} + +func TestDelete(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + test.TestDelete(validNewConfigMap()) +} + +func TestGet(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + test.TestGet(validNewConfigMap()) +} + +func TestList(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + test.TestList(validNewConfigMap()) +} + +func TestWatch(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Etcd) + test.TestWatch( + validNewConfigMap(), + // matching labels + []labels.Set{ + {"label-1": "value-1"}, + {"label-2": "value-2"}, + }, + // not matching labels + []labels.Set{ + {"foo": "bar"}, + }, + // matching fields + []fields.Set{ + {"metadata.namespace": "default"}, + {"metadata.name": "foo"}, + }, + // not matching fields + []fields.Set{ + {"metadata.name": "bar"}, + {"name": "foo"}, + }, + ) +} diff --git a/pkg/registry/configmap/registry.go b/pkg/registry/configmap/registry.go new file mode 100644 index 00000000000..53968e03b31 --- /dev/null +++ b/pkg/registry/configmap/registry.go @@ -0,0 +1,92 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/watch" +) + +// Registry is an interface for things that know how to store ConfigMaps. +type Registry interface { + ListConfigMaps(ctx api.Context, options *unversioned.ListOptions) (*extensions.ConfigMapList, error) + WatchConfigMaps(ctx api.Context, options *unversioned.ListOptions) (watch.Interface, error) + GetConfigMap(ctx api.Context, name string) (*extensions.ConfigMap, error) + CreateConfigMap(ctx api.Context, cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) + UpdateConfigMap(ctx api.Context, cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) + DeleteConfigMap(ctx api.Context, name string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListConfigMaps(ctx api.Context, options *unversioned.ListOptions) (*extensions.ConfigMapList, error) { + obj, err := s.List(ctx, options) + if err != nil { + return nil, err + } + + return obj.(*extensions.ConfigMapList), err +} + +func (s *storage) WatchConfigMaps(ctx api.Context, options *unversioned.ListOptions) (watch.Interface, error) { + return s.Watch(ctx, options) +} + +func (s *storage) GetConfigMap(ctx api.Context, name string) (*extensions.ConfigMap, error) { + obj, err := s.Get(ctx, name) + if err != nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), nil +} + +func (s *storage) CreateConfigMap(ctx api.Context, cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + obj, err := s.Create(ctx, cfg) + if err != nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), nil +} + +func (s *storage) UpdateConfigMap(ctx api.Context, cfg *extensions.ConfigMap) (*extensions.ConfigMap, error) { + obj, _, err := s.Update(ctx, cfg) + if err != nil { + return nil, err + } + + return obj.(*extensions.ConfigMap), nil +} + +func (s *storage) DeleteConfigMap(ctx api.Context, name string) error { + _, err := s.Delete(ctx, name, nil) + + return err +} diff --git a/pkg/registry/configmap/strategy.go b/pkg/registry/configmap/strategy.go new file mode 100644 index 00000000000..c10ee51b1e2 --- /dev/null +++ b/pkg/registry/configmap/strategy.go @@ -0,0 +1,105 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apis/extensions/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// strategy implements behavior for ConfigMap objects +type strategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating ConfigMap +// objects via the REST API. +var Strategy = strategy{api.Scheme, api.SimpleNameGenerator} + +// Strategy should implement rest.RESTCreateStrategy +var _ rest.RESTCreateStrategy = Strategy + +// Strategy should implement rest.RESTUpdateStrategy +var _ rest.RESTUpdateStrategy = Strategy + +func (strategy) NamespaceScoped() bool { + return true +} + +func (strategy) PrepareForCreate(obj runtime.Object) { + _ = obj.(*extensions.ConfigMap) +} + +func (strategy) Validate(ctx api.Context, obj runtime.Object) field.ErrorList { + cfg := obj.(*extensions.ConfigMap) + + return validation.ValidateConfigMap(cfg) +} + +// Canonicalize normalizes the object after validation. +func (strategy) Canonicalize(obj runtime.Object) { +} + +func (strategy) AllowCreateOnUpdate() bool { + return false +} + +func (strategy) PrepareForUpdate(newObj, oldObj runtime.Object) { + _ = oldObj.(*extensions.ConfigMap) + _ = newObj.(*extensions.ConfigMap) +} + +func (strategy) AllowUnconditionalUpdate() bool { + return true +} + +func (strategy) ValidateUpdate(ctx api.Context, newObj, oldObj runtime.Object) field.ErrorList { + oldCfg, newCfg := oldObj.(*extensions.ConfigMap), newObj.(*extensions.ConfigMap) + + return validation.ValidateConfigMapUpdate(newCfg, oldCfg) +} + +// ConfigMapToSelectableFields returns a field set that represents the object for matching purposes. +func ConfigMapToSelectableFields(cfg *extensions.ConfigMap) fields.Set { + return generic.ObjectMetaFieldsSet(cfg.ObjectMeta, true) +} + +// MatchConfigMap returns a generic matcher for a given label and field selector. +func MatchConfigMap(label labels.Selector, field fields.Selector) generic.Matcher { + return &generic.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { + cfg, ok := obj.(*extensions.ConfigMap) + if !ok { + return nil, nil, fmt.Errorf("given object is not of type ConfigMap") + } + + return labels.Set(cfg.ObjectMeta.Labels), ConfigMapToSelectableFields(cfg), nil + }, + } +} diff --git a/pkg/registry/configmap/strategy_test.go b/pkg/registry/configmap/strategy_test.go new file mode 100644 index 00000000000..8cc4734222e --- /dev/null +++ b/pkg/registry/configmap/strategy_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2015 The Kubernetes Authors 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 configmap + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestConfigMapStrategy(t *testing.T) { + ctx := api.NewDefaultContext() + if !Strategy.NamespaceScoped() { + t.Errorf("ConfigMap must be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("ConfigMap should not allow create on update") + } + + cfg := &extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "valid-config-data", + Namespace: api.NamespaceDefault, + }, + Data: map[string]string{ + "foo": "bar", + }, + } + + Strategy.PrepareForCreate(cfg) + + errs := Strategy.Validate(ctx, cfg) + if len(errs) != 0 { + t.Errorf("unexpected error validating %v", errs) + } + + newCfg := &extensions.ConfigMap{ + ObjectMeta: api.ObjectMeta{ + Name: "valid-config-data-2", + Namespace: api.NamespaceDefault, + ResourceVersion: "4", + }, + Data: map[string]string{ + "invalidKey": "updatedValue", + }, + } + + Strategy.PrepareForUpdate(newCfg, cfg) + + errs = Strategy.ValidateUpdate(ctx, newCfg, cfg) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +}