diff --git a/examples/examples_test.go b/examples/examples_test.go index d21c1eea150..dbf19b025a8 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -34,7 +34,6 @@ import ( ) func validateObject(obj runtime.Object) (errors []error) { - ctx := api.NewDefaultContext() switch t := obj.(type) { case *api.ReplicationController: if t.Namespace == "" { @@ -49,7 +48,6 @@ func validateObject(obj runtime.Object) (errors []error) { if t.Namespace == "" { t.Namespace = api.NamespaceDefault } - api.ValidNamespace(ctx, &t.ObjectMeta) errors = validation.ValidateService(t) case *api.ServiceList: for i := range t.Items { @@ -59,7 +57,6 @@ func validateObject(obj runtime.Object) (errors []error) { if t.Namespace == "" { t.Namespace = api.NamespaceDefault } - api.ValidNamespace(ctx, &t.ObjectMeta) errors = validation.ValidatePod(t) case *api.PodList: for i := range t.Items { @@ -68,8 +65,15 @@ func validateObject(obj runtime.Object) (errors []error) { case *api.PersistentVolume: errors = validation.ValidatePersistentVolume(t) case *api.PersistentVolumeClaim: - api.ValidNamespace(ctx, &t.ObjectMeta) + if t.Namespace == "" { + t.Namespace = api.NamespaceDefault + } errors = validation.ValidatePersistentVolumeClaim(t) + case *api.PodTemplate: + if t.Namespace == "" { + t.Namespace = api.NamespaceDefault + } + errors = validation.ValidatePodTemplate(t) default: return []error{fmt.Errorf("no validation defined for %#v", obj)} } @@ -156,6 +160,7 @@ func TestExampleObjectSchemas(t *testing.T) { "pod-with-http-healthcheck": &api.Pod{}, "service": &api.Service{}, "replication-controller": &api.ReplicationController{}, + "podtemplate": &api.PodTemplate{}, }, "../examples/update-demo": { "kitten-rc": &api.ReplicationController{}, diff --git a/examples/walkthrough/podtemplate.json b/examples/walkthrough/podtemplate.json new file mode 100644 index 00000000000..0fbdeb37de2 --- /dev/null +++ b/examples/walkthrough/podtemplate.json @@ -0,0 +1,22 @@ + { + "apiVersion": "v1beta3", + "kind": "PodTemplate", + "metadata": { + "name": "nginx" + }, + "spec": { + "metadata": { + "labels": { + "name": "nginx" + }, + "generateName": "nginx-" + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "dockerfile/nginx", + "ports": [{"containerPort": 80}] + }] + } + } + } diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index b86756a9a02..3e534329de2 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -358,6 +358,34 @@ for version in "${kube_api_versions[@]}"; do kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" '' + ################# + # Pod templates # + ################# + + # Note: pod templates exist only in v1beta3 and above, so output will always be in that form + + ### Create PODTEMPLATE + # Pre-condition: no PODTEMPLATE + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" '' + # Command + kubectl create -f examples/walkthrough/podtemplate.json "${kube_flags[@]}" + # Post-condition: nginx PODTEMPLATE is available + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" 'nginx:' + + ### Printing pod templates works + kubectl get podtemplates "${kube_flags[@]}" + ### Display of an object which doesn't existing in v1beta1 and v1beta2 works + [[ "$(kubectl get podtemplates -o yaml "${kube_flags[@]}" | grep nginx)" ]] + + ### Delete nginx pod template by name + # Pre-condition: nginx pod template is available + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" 'nginx:' + # Command + kubectl delete podtemplate nginx "${kube_flags[@]}" + # Post-condition: No templates exist + kube::test::get_object_assert podtemplate "{{range.items}}{{.metadata.name}}:{{end}}" '' + + ############ # Services # ############ diff --git a/pkg/api/latest/latest_test.go b/pkg/api/latest/latest_test.go index 724f5ee59d9..6b1591b5117 100644 --- a/pkg/api/latest/latest_test.go +++ b/pkg/api/latest/latest_test.go @@ -80,6 +80,10 @@ func TestRESTMapper(t *testing.T) { t.Errorf("unexpected version mapping: %s %s %v", v, k, err) } + if m, err := RESTMapper.RESTMapping("PodTemplate", ""); err != nil || m.APIVersion != "v1beta3" || m.Resource != "podtemplates" { + t.Errorf("unexpected version mapping: %#v %v", m, err) + } + for _, version := range Versions { mapping, err := RESTMapper.RESTMapping("ReplicationController", version) if err != nil { diff --git a/pkg/api/register.go b/pkg/api/register.go index 83fc40e23af..823f8bb7685 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -28,6 +28,8 @@ func init() { &Pod{}, &PodList{}, &PodStatusResult{}, + &PodTemplate{}, + &PodTemplateList{}, &ReplicationControllerList{}, &ReplicationController{}, &ServiceList{}, @@ -71,6 +73,8 @@ func init() { func (*Pod) IsAnAPIObject() {} func (*PodList) IsAnAPIObject() {} func (*PodStatusResult) IsAnAPIObject() {} +func (*PodTemplate) IsAnAPIObject() {} +func (*PodTemplateList) IsAnAPIObject() {} func (*ReplicationController) IsAnAPIObject() {} func (*ReplicationControllerList) IsAnAPIObject() {} func (*Service) IsAnAPIObject() {} diff --git a/pkg/api/rest/resttest/resttest.go b/pkg/api/rest/resttest/resttest.go index 29585d7dc7e..2732a0e563f 100644 --- a/pkg/api/rest/resttest/resttest.go +++ b/pkg/api/rest/resttest/resttest.go @@ -179,7 +179,7 @@ func (t *Tester) TestCreateRejectsMismatchedNamespace(valid runtime.Object) { if err == nil { t.Errorf("Expected an error, but we didn't get one") } else if strings.Contains(err.Error(), "Controller.Namespace does not match the provided context") { - t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) + t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err) } } @@ -195,7 +195,30 @@ func (t *Tester) TestCreateRejectsNamespace(valid runtime.Object) { if err == nil { t.Errorf("Expected an error, but we didn't get one") } else if strings.Contains(err.Error(), "Controller.Namespace does not match the provided context") { - t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) + t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err) + } +} + +func (t *Tester) TestUpdate(valid runtime.Object, existing, older runtime.Object) { + t.TestUpdateFailsOnNotFound(copyOrDie(valid)) + t.TestUpdateFailsOnVersion(copyOrDie(older)) +} + +func (t *Tester) TestUpdateFailsOnNotFound(valid runtime.Object) { + _, _, err := t.storage.(rest.Updater).Update(api.NewDefaultContext(), valid) + if err == nil { + t.Errorf("Expected an error, but we didn't get one") + } else if !errors.IsNotFound(err) { + t.Errorf("Expected NotFound error, got '%v'", err) + } +} + +func (t *Tester) TestUpdateFailsOnVersion(older runtime.Object) { + _, _, err := t.storage.(rest.Updater).Update(api.NewDefaultContext(), older) + if err == nil { + t.Errorf("Expected an error, but we didn't get one") + } else if !errors.IsConflict(err) { + t.Errorf("Expected Conflict error, got '%v'", err) } } diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index ca79e6f9408..e9f65233307 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -85,13 +85,20 @@ func roundTrip(t *testing.T, codec runtime.Codec, item runtime.Object) { } // roundTripSame verifies the same source object is tested in all API versions. -func roundTripSame(t *testing.T, item runtime.Object) { +func roundTripSame(t *testing.T, item runtime.Object, except ...string) { + set := util.NewStringSet(except...) seed := rand.Int63() fuzzInternalObject(t, "", item, seed) - roundTrip(t, v1beta1.Codec, item) - roundTrip(t, v1beta2.Codec, item) - fuzzInternalObject(t, "v1beta3", item, seed) - roundTrip(t, v1beta3.Codec, item) + if !set.Has("v1beta1") { + roundTrip(t, v1beta1.Codec, item) + } + if !set.Has("v1beta2") { + roundTrip(t, v1beta2.Codec, item) + } + if !set.Has("v1beta3") { + fuzzInternalObject(t, "v1beta3", item, seed) + roundTrip(t, v1beta3.Codec, item) + } } func roundTripAll(t *testing.T, item runtime.Object) { @@ -130,6 +137,10 @@ func TestList(t *testing.T) { var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList") var nonInternalRoundTrippableTypes = util.NewStringSet("List", "ListOptions", "PodExecOptions") +var nonRoundTrippableTypesByVersion = map[string][]string{ + "PodTemplate": {"v1beta1", "v1beta2"}, + "PodTemplateList": {"v1beta1", "v1beta2"}, +} func TestRoundTripTypes(t *testing.T) { // api.Scheme.Log(t) @@ -148,7 +159,7 @@ func TestRoundTripTypes(t *testing.T) { if _, err := meta.TypeAccessor(item); err != nil { t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableTypes: %v", kind, err) } - roundTripSame(t, item) + roundTripSame(t, item, nonRoundTrippableTypesByVersion[kind]...) if !nonInternalRoundTrippableTypes.Has(kind) { roundTrip(t, api.Codec, fuzzInternalObject(t, "", item, rand.Int63())) } diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index a1445e71292..1758375ddc8 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -888,6 +888,24 @@ func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) errs.ValidationErrorList { return allErrs } +// ValidatePodTemplate tests if required fields in the pod template are set. +func ValidatePodTemplate(pod *api.PodTemplate) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Spec, 0).Prefix("spec")...) + return allErrs +} + +// ValidatePodTemplateUpdate tests to see if the update is legal for an end user to make. newPod is updated with fields +// that cannot be changed. +func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Spec, 0).Prefix("spec")...) + return allErrs +} + var supportedSessionAffinityType = util.NewStringSet(string(api.AffinityTypeClientIP), string(api.AffinityTypeNone)) // ValidateService tests if required fields in the service are set. diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 31aa674efd3..2b10dd93aaf 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -245,6 +245,7 @@ func (h *HumanReadablePrinter) HandledResources() []string { } var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED", "MESSAGE"} +var podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLABELS"} var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS"} var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT(S)"} var endpointColumns = []string{"NAME", "ENDPOINTS"} @@ -263,6 +264,8 @@ var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"} func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(podColumns, printPod) h.Handler(podColumns, printPodList) + h.Handler(podTemplateColumns, printPodTemplate) + h.Handler(podTemplateColumns, printPodTemplateList) h.Handler(replicationControllerColumns, printReplicationController) h.Handler(replicationControllerColumns, printReplicationControllerList) h.Handler(serviceColumns, printService) @@ -383,11 +386,6 @@ func interpretContainerStatus(status *api.ContainerStatus) (string, string, stri } func printPod(pod *api.Pod, w io.Writer) error { - // TODO: remove me when pods are converted - spec := &api.PodSpec{} - if err := api.Scheme.Convert(&pod.Spec, spec); err != nil { - glog.Errorf("Unable to convert pod manifest: %v", err) - } _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", pod.Name, pod.Status.PodIP, @@ -447,6 +445,40 @@ func printPodList(podList *api.PodList, w io.Writer) error { return nil } +func printPodTemplate(pod *api.PodTemplate, w io.Writer) error { + containers := pod.Spec.Spec.Containers + var firstContainer api.Container + if len(containers) > 0 { + firstContainer, containers = containers[0], containers[1:] + } + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", + pod.Name, + firstContainer.Name, + firstContainer.Image, + formatLabels(pod.Spec.Labels), + ) + if err != nil { + return err + } + // Lay out all the other containers on separate lines. + for _, container := range containers { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "", container.Name, container.Image, "") + if err != nil { + return err + } + } + return nil +} + +func printPodTemplateList(podList *api.PodTemplateList, w io.Writer) error { + for _, pod := range podList.Items { + if err := printPodTemplate(&pod, w); err != nil { + return err + } + } + return nil +} + func printReplicationController(controller *api.ReplicationController, w io.Writer) error { containers := controller.Spec.Template.Spec.Containers var firstContainer api.Container diff --git a/pkg/master/master.go b/pkg/master/master.go index 0b4c582f978..b8440e52d03 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -58,6 +58,7 @@ import ( pvcetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/persistentvolumeclaim/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" podetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod/etcd" + podtemplateetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/podtemplate/etcd" resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" @@ -360,9 +361,18 @@ func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter) // init initializes master. func (m *Master) init(c *Config) { + // TODO: make initialization of the helper part of the Master, and allow some storage + // objects to have a newer storage version than the user's default. + newerHelper, err := NewEtcdHelper(c.EtcdHelper.Client, "v1beta3") + if err != nil { + glog.Fatalf("Unable to setup storage for v1beta3: %v", err) + } + podStorage := podetcd.NewStorage(c.EtcdHelper, c.KubeletClient) podRegistry := pod.NewRegistry(podStorage.Pod) + podTemplateStorage := podtemplateetcd.NewREST(newerHelper) + eventRegistry := event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())) limitRangeRegistry := limitrange.NewEtcdRegistry(c.EtcdHelper) @@ -397,6 +407,8 @@ func (m *Master) init(c *Config) { "pods/binding": podStorage.Binding, "bindings": podStorage.Binding, + "podTemplates": podTemplateStorage, + "replicationControllers": controllerStorage, "services": service.NewStorage(m.serviceRegistry, m.nodeRegistry, m.endpointRegistry, m.portalNet, c.ClusterName), "endpoints": endpointsStorage, @@ -606,6 +618,9 @@ func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion { func (m *Master) api_v1beta1() *apiserver.APIGroupVersion { storage := make(map[string]rest.Storage) for k, v := range m.storage { + if k == "podTemplates" { + continue + } storage[k] = v } version := m.defaultAPIGroupVersion() @@ -619,6 +634,9 @@ func (m *Master) api_v1beta1() *apiserver.APIGroupVersion { func (m *Master) api_v1beta2() *apiserver.APIGroupVersion { storage := make(map[string]rest.Storage) for k, v := range m.storage { + if k == "podTemplates" { + continue + } storage[k] = v } version := m.defaultAPIGroupVersion() diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 33738073cd8..1cab2036331 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -35,7 +35,6 @@ import ( ) // podStrategy implements behavior for Pods -// TODO: move to a pod specific package. type podStrategy struct { runtime.ObjectTyper api.NameGenerator diff --git a/pkg/registry/podtemplate/doc.go b/pkg/registry/podtemplate/doc.go new file mode 100644 index 00000000000..3be9642c4d0 --- /dev/null +++ b/pkg/registry/podtemplate/doc.go @@ -0,0 +1,18 @@ +/* +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 podtemplate provides RESTStorage implementations for storing PodTemplate API objects. +package podtemplate diff --git a/pkg/registry/podtemplate/etcd/etcd.go b/pkg/registry/podtemplate/etcd/etcd.go new file mode 100644 index 00000000000..421ddb8cbb9 --- /dev/null +++ b/pkg/registry/podtemplate/etcd/etcd.go @@ -0,0 +1,63 @@ +/* +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 etcd + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/podtemplate" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// rest implements a RESTStorage for pod templates against etcd +type REST struct { + etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against pod templates. +func NewREST(h tools.EtcdHelper) *REST { + prefix := "/registry/podtemplates" + store := etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.PodTemplate{} }, + NewListFunc: func() runtime.Object { return &api.PodTemplateList{} }, + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.PodTemplate).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return podtemplate.MatchPodTemplate(label, field) + }, + EndpointName: "podtemplates", + + CreateStrategy: podtemplate.Strategy, + UpdateStrategy: podtemplate.Strategy, + ReturnDeletedObject: true, + + Helper: h, + } + + return &REST{store} +} diff --git a/pkg/registry/podtemplate/etcd/etcd_test.go b/pkg/registry/podtemplate/etcd/etcd_test.go new file mode 100644 index 00000000000..b3b01c8b711 --- /dev/null +++ b/pkg/registry/podtemplate/etcd/etcd_test.go @@ -0,0 +1,99 @@ +/* +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 etcd + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.NewEtcdHelper(fakeEtcdClient, v1beta3.Codec) + return fakeEtcdClient, helper +} + +func validNewPodTemplate(name string) *api.PodTemplate { + return &api.PodTemplate{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: api.NamespaceDefault, + }, + Spec: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"test": "foo"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{ + { + Name: "foo", + Image: "test", + ImagePullPolicy: api.PullAlways, + + TerminationMessagePath: api.TerminationMessagePathDefault, + }, + }, + }, + }, + } +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + pod := validNewPodTemplate("foo") + pod.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + pod, + // invalid + &api.PodTemplate{ + Spec: api.PodTemplateSpec{}, + }, + ) +} + +func TestUpdate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + + fakeEtcdClient.ExpectNotFoundGet("/registry/podtemplates/default/foo") + fakeEtcdClient.ChangeIndex = 2 + pod := validNewPodTemplate("foo") + existing := validNewPodTemplate("exists") + obj, err := storage.Create(api.NewDefaultContext(), existing) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + older := obj.(*api.PodTemplate) + older.ResourceVersion = "1" + + test.TestUpdate( + pod, + existing, + older, + ) +} diff --git a/pkg/registry/podtemplate/rest.go b/pkg/registry/podtemplate/rest.go new file mode 100644 index 00000000000..9204c098e75 --- /dev/null +++ b/pkg/registry/podtemplate/rest.go @@ -0,0 +1,81 @@ +/* +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 podtemplate + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + errs "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +// podTemplateStrategy implements behavior for PodTemplates +type podTemplateStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating PodTemplate +// objects via the REST API. +var Strategy = podTemplateStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is true for pod templates. +func (podTemplateStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears fields that are not allowed to be set by end users on creation. +func (podTemplateStrategy) PrepareForCreate(obj runtime.Object) { + _ = obj.(*api.PodTemplate) +} + +// Validate validates a new pod template. +func (podTemplateStrategy) Validate(ctx api.Context, obj runtime.Object) errs.ValidationErrorList { + pod := obj.(*api.PodTemplate) + return validation.ValidatePodTemplate(pod) +} + +// AllowCreateOnUpdate is false for pod templates. +func (podTemplateStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (podTemplateStrategy) PrepareForUpdate(obj, old runtime.Object) { + _ = obj.(*api.PodTemplate) +} + +// ValidateUpdate is the default update validation for an end user. +func (podTemplateStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) errs.ValidationErrorList { + return validation.ValidatePodTemplateUpdate(obj.(*api.PodTemplate), old.(*api.PodTemplate)) +} + +// MatchPodTemplate returns a generic matcher for a given label and field selector. +func MatchPodTemplate(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + podObj, ok := obj.(*api.PodTemplate) + if !ok { + return false, fmt.Errorf("not a pod template") + } + return label.Matches(labels.Set(podObj.Labels)), nil + }) +}