diff --git a/examples/examples_test.go b/examples/examples_test.go index 8c5ce092e53..4cbf5aa615c 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -65,6 +65,11 @@ func validateObject(obj runtime.Object) (errors []error) { for i := range t.Items { errors = append(errors, validateObject(&t.Items[i])...) } + case *api.PersistentVolume: + errors = validation.ValidatePersistentVolume(t) + case *api.PersistentVolumeClaim: + api.ValidNamespace(ctx, &t.ObjectMeta) + errors = validation.ValidatePersistentVolumeClaim(t) default: return []error{fmt.Errorf("no validation defined for %#v", obj)} } @@ -160,6 +165,16 @@ func TestExampleObjectSchemas(t *testing.T) { "kitten-rc": &api.ReplicationController{}, "nautilus-rc": &api.ReplicationController{}, }, + "../examples/persistent-volumes/volumes": { + "local-01": &api.PersistentVolume{}, + "local-02": &api.PersistentVolume{}, + "gce": &api.PersistentVolume{}, + }, + "../examples/persistent-volumes/claims": { + "claim-01": &api.PersistentVolumeClaim{}, + "claim-02": &api.PersistentVolumeClaim{}, + "claim-03": &api.PersistentVolumeClaim{}, + }, } for path, expected := range cases { diff --git a/examples/persistent-volumes/claims/claim-01.yaml b/examples/persistent-volumes/claims/claim-01.yaml new file mode 100644 index 00000000000..cb0a2abb342 --- /dev/null +++ b/examples/persistent-volumes/claims/claim-01.yaml @@ -0,0 +1,10 @@ +kind: PersistentVolumeClaim +apiVersion: v1beta3 +metadata: + name: myclaim-1 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 3 diff --git a/examples/persistent-volumes/claims/claim-02.yaml b/examples/persistent-volumes/claims/claim-02.yaml new file mode 100644 index 00000000000..134ae4cf972 --- /dev/null +++ b/examples/persistent-volumes/claims/claim-02.yaml @@ -0,0 +1,10 @@ +kind: PersistentVolumeClaim +apiVersion: v1beta3 +metadata: + name: myclaim-2 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8 diff --git a/examples/persistent-volumes/claims/claim-03.json b/examples/persistent-volumes/claims/claim-03.json new file mode 100644 index 00000000000..b3b0717af09 --- /dev/null +++ b/examples/persistent-volumes/claims/claim-03.json @@ -0,0 +1,17 @@ +{ + "kind": "PersistentVolumeClaim", + "apiVersion": "v1beta3", + "metadata": { + "name": "myclaim-3" + }, "spec": { + "accessModes": [ + "ReadWriteOnce", + "ReadOnlyMany" + ], + "resources": { + "requests": { + "storage": "10G" + } + } + } +} diff --git a/examples/persistent-volumes/simpletest/README.md b/examples/persistent-volumes/simpletest/README.md new file mode 100644 index 00000000000..026920d172b --- /dev/null +++ b/examples/persistent-volumes/simpletest/README.md @@ -0,0 +1,69 @@ +# How To Use Persistent Volumes + +This guide assumes knowledge of Kubernetes fundamentals and that a user has a cluster up and running. + +## Create volumes + +Persistent Volumes are intended for "network volumes", such as GCE Persistent Disks, NFS shares, and AWS EBS volumes. + +The `HostPath` VolumeSource was included in the Persistent Volumes implementation for ease of testing. + +Create persistent volumes by posting them to the API server: + +``` + +cluster/kubectl.sh create -f examples/persistent-volumes/volumes/local-01.yaml +cluster/kubectl.sh create -f examples/persistent-volumes/volumes/local-02.yaml + +cluster/kubectl.sh get pv + +NAME LABELS CAPACITY ACCESSMODES STATUS CLAIM +pv0001 map[] 10737418240 RWO +pv0002 map[] 5368709120 RWO + + +In the log: + +I0302 10:20:45.663225 1920 persistent_volume_manager.go:115] Managing PersistentVolume[UID=b16e91d6-c0ef-11e4-8be4-80e6500a981e] +I0302 10:20:55.667945 1920 persistent_volume_manager.go:115] Managing PersistentVolume[UID=b41f4f0e-c0ef-11e4-8be4-80e6500a981e] + +``` + +## Create claims + +You must be in a namespace to create claims. + +``` + +cluster/kubectl.sh create -f examples/persistent-volumes/claims/claim-01.yaml +cluster/kubectl.sh create -f examples/persistent-volumes/claims/claim-02.yaml + +NAME LABELS STATUS VOLUME +myclaim-1 map[] +myclaim-2 map[] + +``` + + +## Matching and binding + +``` + +PersistentVolumeClaim[UID=f4b3d283-c0ef-11e4-8be4-80e6500a981e] bound to PersistentVolume[UID=b16e91d6-c0ef-11e4-8be4-80e6500a981e] + + + +cluster/kubectl.sh get pv + +NAME LABELS CAPACITY ACCESSMODES STATUS CLAIM +pv0001 map[] 10737418240 RWO myclaim-1 / f4b3d283-c0ef-11e4-8be4-80e6500a981e +pv0002 map[] 5368709120 RWO myclaim-2 / f70da891-c0ef-11e4-8be4-80e6500a981e + + +cluster/kubectl.sh get pvc + +NAME LABELS STATUS VOLUME +myclaim-1 map[] b16e91d6-c0ef-11e4-8be4-80e6500a981e +myclaim-2 map[] b41f4f0e-c0ef-11e4-8be4-80e6500a981e + +``` diff --git a/examples/persistent-volumes/simpletest/namespace.json b/examples/persistent-volumes/simpletest/namespace.json new file mode 100644 index 00000000000..c9e7ced5557 --- /dev/null +++ b/examples/persistent-volumes/simpletest/namespace.json @@ -0,0 +1,10 @@ +{ + "kind": "Namespace", + "apiVersion":"v1beta3", + "metadata": { + "name": "myns", + "labels": { + "name": "development" + } + } +} diff --git a/examples/persistent-volumes/simpletest/pod.yaml b/examples/persistent-volumes/simpletest/pod.yaml new file mode 100644 index 00000000000..bb76ff4f74b --- /dev/null +++ b/examples/persistent-volumes/simpletest/pod.yaml @@ -0,0 +1,18 @@ +kind: Pod +apiVersion: v1beta3 +metadata: + name: mypod +spec: + containers: + - image: dockerfile/nginx + name: myfrontend + volumeMounts: + - mountPath: "/var/www/html" + name: mypd + volumes: + - name: mypd + source: + persistentVolumeClaim: + accessMode: ReadWriteOnce + claimRef: + name: myclaim-1 diff --git a/examples/persistent-volumes/volumes/gce.yaml b/examples/persistent-volumes/volumes/gce.yaml new file mode 100644 index 00000000000..3e124c59c9f --- /dev/null +++ b/examples/persistent-volumes/volumes/gce.yaml @@ -0,0 +1,10 @@ +kind: PersistentVolume +apiVersion: v1beta3 +metadata: + name: pv0003 +spec: + capacity: + storage: 10 + gcePersistentDisk: + pdName: "abc123" + fsType: "ext4" diff --git a/examples/persistent-volumes/volumes/local-01.yaml b/examples/persistent-volumes/volumes/local-01.yaml new file mode 100644 index 00000000000..105e4393ebb --- /dev/null +++ b/examples/persistent-volumes/volumes/local-01.yaml @@ -0,0 +1,11 @@ +kind: PersistentVolume +apiVersion: v1beta3 +metadata: + name: pv0001 + labels: + type: local +spec: + capacity: + storage: 10Gi + hostPath: + path: "/tmp/data01" diff --git a/examples/persistent-volumes/volumes/local-02.yaml b/examples/persistent-volumes/volumes/local-02.yaml new file mode 100644 index 00000000000..1f40d7a03e4 --- /dev/null +++ b/examples/persistent-volumes/volumes/local-02.yaml @@ -0,0 +1,11 @@ +kind: PersistentVolume +apiVersion: v1beta3 +metadata: + name: pv0002 + labels: + type: local +spec: + capacity: + storage: 5Gi + hostPath: + path: "/tmp/data02" diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 5338b9110a3..d57b487db9c 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -529,6 +529,49 @@ __EOF__ # Post-condition: no replication controller is running kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' + ###################### + # Persistent Volumes # + ###################### + + ### Create and delete persistent volume examples + # Pre-condition: no persistent volumes currently exist + kube::test::get_object_assert pv "{{range.items}}{{.$id_field}}:{{end}}" '' + # Command + kubectl create -f examples/persistent-volumes/volumes/local-01.yaml "${kube_flags[@]}" + kube::test::get_object_assert pv "{{range.items}}{{.$id_field}}:{{end}}" 'pv0001:' + kubectl delete pv pv0001 "${kube_flags[@]}" + kubectl create -f examples/persistent-volumes/volumes/local-02.yaml "${kube_flags[@]}" + kube::test::get_object_assert pv "{{range.items}}{{.$id_field}}:{{end}}" 'pv0002:' + kubectl delete pv pv0002 "${kube_flags[@]}" + kubectl create -f examples/persistent-volumes/volumes/gce.yaml "${kube_flags[@]}" + kube::test::get_object_assert pv "{{range.items}}{{.$id_field}}:{{end}}" 'pv0003:' + kubectl delete pv pv0003 "${kube_flags[@]}" + # Post-condition: no PVs + kube::test::get_object_assert pv "{{range.items}}{{.$id_field}}:{{end}}" '' + + ############################ + # Persistent Volume Claims # + ############################ + + ### Create and delete persistent volume claim examples + # Pre-condition: no persistent volume claims currently exist + kube::test::get_object_assert pvc "{{range.items}}{{.$id_field}}:{{end}}" '' + # Command + kubectl create -f examples/persistent-volumes/claims/claim-01.yaml "${kube_flags[@]}" + kube::test::get_object_assert pvc "{{range.items}}{{.$id_field}}:{{end}}" 'myclaim-1:' + kubectl delete pvc myclaim-1 "${kube_flags[@]}" + + kubectl create -f examples/persistent-volumes/claims/claim-02.yaml "${kube_flags[@]}" + kube::test::get_object_assert pvc "{{range.items}}{{.$id_field}}:{{end}}" 'myclaim-2:' + kubectl delete pvc myclaim-2 "${kube_flags[@]}" + + kubectl create -f examples/persistent-volumes/claims/claim-03.json "${kube_flags[@]}" + kube::test::get_object_assert pvc "{{range.items}}{{.$id_field}}:{{end}}" 'myclaim-3:' + kubectl delete pvc myclaim-3 "${kube_flags[@]}" + # Post-condition: no PVCs + kube::test::get_object_assert pvc "{{range.items}}{{.$id_field}}:{{end}}" '' + + ######### # Nodes # diff --git a/pkg/api/types.go b/pkg/api/types.go index 39c82bad83f..994993f972e 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -224,8 +224,9 @@ type PersistentVolumeSpec struct { // Resources represents the actual resources of the volume Capacity ResourceList `json:"capacity` // Source represents the location and type of a volume to mount. - // AccessModeTypes are inferred from the Source. PersistentVolumeSource `json:",inline"` + // AccessModes contains all ways the volume can be mounted + AccessModes []AccessModeType `json:"accessModes,omitempty"` // holds the binding reference to a PersistentVolumeClaim ClaimRef *ObjectReference `json:"claimRef,omitempty"` } diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index d747183c690..737b48dccf3 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -139,8 +139,9 @@ type PersistentVolumeSpec struct { // Resources represents the actual resources of the volume Capacity ResourceList `json:"capacity,omitempty" description:"a description of the persistent volume's resources and capacity"` // Source represents the location and type of a volume to mount. - // AccessModeTypes are inferred from the Source. PersistentVolumeSource `json:",inline" description:"the actual volume backing the persistent volume"` + // AccessModes contains all ways the volume can be mounted + AccessModes []AccessModeType `json:"accessModes,omitempty" description:"all ways the volume can be mounted"` // holds the binding reference to a PersistentVolumeClaim ClaimRef *ObjectReference `json:"claimRef,omitempty" description:"the binding reference to a persistent volume claim"` } diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index ab0db90316d..4c5a1fad828 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -108,8 +108,9 @@ type PersistentVolumeSpec struct { // Resources represents the actual resources of the volume Capacity ResourceList `json:"capacity,omitempty" description:"a description of the persistent volume's resources and capacity"` // Source represents the location and type of a volume to mount. - // AccessModeTypes are inferred from the Source. PersistentVolumeSource `json:",inline" description:"the actual volume backing the persistent volume"` + // AccessModes contains all ways the volume can be mounted + AccessModes []AccessModeType `json:"accessModes,omitempty" description:"all ways the volume can be mounted"` // holds the binding reference to a PersistentVolumeClaim ClaimRef *ObjectReference `json:"claimRef,omitempty" description:"the binding reference to a persistent volume claim"` } diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 2d980f99040..b71f0505dc5 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -241,8 +241,9 @@ type PersistentVolumeSpec struct { // Resources represents the actual resources of the volume Capacity ResourceList `json:"capacity,omitempty" description:"a description of the persistent volume's resources and capacity"` // Source represents the location and type of a volume to mount. - // AccessModeTypes are inferred from the Source. PersistentVolumeSource `json:",inline" description:"the actual volume backing the persistent volume"` + // AccessModes contains all ways the volume can be mounted + AccessModes []AccessModeType `json:"accessModes,omitempty" description:"all ways the volume can be mounted"` // holds the binding reference to a PersistentVolumeClaim ClaimRef *ObjectReference `json:"claimRef,omitempty" description:"the binding reference to a persistent volume claim"` } diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 6a7dcd23825..50c19ca7f2a 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -364,11 +364,12 @@ func validateNFS(nfs *api.NFSVolumeSource) errs.ValidationErrorList { } func ValidatePersistentVolumeName(name string, prefix bool) (bool, string) { - return util.IsDNS1123Label(name), name + return nameIsDNSSubdomain(name, prefix) } func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList { - allErrs := ValidateObjectMeta(&pv.ObjectMeta, false, ValidatePersistentVolumeName) + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&pv.ObjectMeta, false, ValidatePersistentVolumeName).Prefix("metadata")...) if len(pv.Spec.Capacity) == 0 { allErrs = append(allErrs, errs.NewFieldRequired("persistentVolume.Capacity")) @@ -393,6 +394,27 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList return allErrs } +// ValidatePersistentVolumeUpdate tests to see if the update is legal for an end user to make. +// newPv is updated with fields that cannot be changed. +func ValidatePersistentVolumeUpdate(newPv, oldPv *api.PersistentVolume) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = ValidatePersistentVolume(newPv) + newPv.Status = oldPv.Status + return allErrs +} + +// ValidatePersistentVolumeStatusUpdate tests to see if the status update is legal for an end user to make. +// newPv is updated with fields that cannot be changed. +func ValidatePersistentVolumeStatusUpdate(newPv, oldPv *api.PersistentVolume) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPv.ObjectMeta, &newPv.ObjectMeta).Prefix("metadata")...) + if newPv.ResourceVersion == "" { + allErrs = append(allErrs, fmt.Errorf("ResourceVersion must be specified")) + } + newPv.Spec = oldPv.Spec + return allErrs +} + func ValidatePersistentVolumeClaim(pvc *api.PersistentVolumeClaim) errs.ValidationErrorList { allErrs := ValidateObjectMeta(&pvc.ObjectMeta, true, ValidatePersistentVolumeName) if len(pvc.Spec.AccessModes) == 0 { @@ -404,6 +426,23 @@ func ValidatePersistentVolumeClaim(pvc *api.PersistentVolumeClaim) errs.Validati return allErrs } +func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *api.PersistentVolumeClaim) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = ValidatePersistentVolumeClaim(newPvc) + newPvc.Status = oldPvc.Status + return allErrs +} + +func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *api.PersistentVolumeClaim) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPvc.ObjectMeta, &newPvc.ObjectMeta).Prefix("metadata")...) + if newPvc.ResourceVersion == "" { + allErrs = append(allErrs, fmt.Errorf("ResourceVersion must be specified")) + } + newPvc.Spec = oldPvc.Spec + return allErrs +} + var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP)) func validatePorts(ports []api.ContainerPort) errs.ValidationErrorList { diff --git a/pkg/client/client.go b/pkg/client/client.go index debd24cc6b3..3903e4324cf 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -41,6 +41,8 @@ type Interface interface { ResourceQuotasNamespacer SecretsNamespacer NamespacesInterface + PersistentVolumesInterface + PersistentVolumeClaimsNamespacer } func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface { @@ -66,7 +68,6 @@ func (c *Client) Pods(namespace string) PodInterface { func (c *Client) Services(namespace string) ServiceInterface { return newServices(c, namespace) } - func (c *Client) LimitRanges(namespace string) LimitRangeInterface { return newLimitRanges(c, namespace) } @@ -83,6 +84,14 @@ func (c *Client) Namespaces() NamespaceInterface { return newNamespaces(c) } +func (c *Client) PersistentVolumes() PersistentVolumeInterface { + return newPersistentVolumes(c) +} + +func (c *Client) PersistentVolumeClaims(namespace string) PersistentVolumeClaimInterface { + return newPersistentVolumeClaims(c, namespace) +} + // 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 e0e496d9b94..42e187dabd1 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -35,22 +35,26 @@ type FakeAction struct { // Fake implements Interface. 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 Fake struct { - Actions []FakeAction - PodsList api.PodList - CtrlList api.ReplicationControllerList - Ctrl api.ReplicationController - ServiceList api.ServiceList - EndpointsList api.EndpointsList - MinionsList api.NodeList - EventsList api.EventList - LimitRangesList api.LimitRangeList - ResourceQuotaStatus api.ResourceQuota - ResourceQuotasList api.ResourceQuotaList - NamespacesList api.NamespaceList - SecretList api.SecretList - Secret api.Secret - Err error - Watch watch.Interface + Actions []FakeAction + PodsList api.PodList + CtrlList api.ReplicationControllerList + Ctrl api.ReplicationController + ServiceList api.ServiceList + EndpointsList api.EndpointsList + MinionsList api.NodeList + EventsList api.EventList + LimitRangesList api.LimitRangeList + ResourceQuotaStatus api.ResourceQuota + ResourceQuotasList api.ResourceQuotaList + NamespacesList api.NamespaceList + SecretList api.SecretList + Secret api.Secret + Err error + Watch watch.Interface + PersistentVolume api.PersistentVolume + PersistentVolumesList api.PersistentVolumeList + PersistentVolumeClaim api.PersistentVolumeClaim + PersistentVolumeClaimList api.PersistentVolumeClaimList } func (c *Fake) LimitRanges(namespace string) LimitRangeInterface { @@ -81,6 +85,14 @@ func (c *Fake) Pods(namespace string) PodInterface { return &FakePods{Fake: c, Namespace: namespace} } +func (c *Fake) PersistentVolumes() PersistentVolumeInterface { + return &FakePersistentVolumes{Fake: c} +} + +func (c *Fake) PersistentVolumeClaims(namespace string) PersistentVolumeClaimInterface { + return &FakePersistentVolumeClaims{Fake: c, Namespace: namespace} +} + func (c *Fake) Services(namespace string) ServiceInterface { return &FakeServices{Fake: c, Namespace: namespace} } diff --git a/pkg/client/fake_persistent_volume_claims.go b/pkg/client/fake_persistent_volume_claims.go new file mode 100644 index 00000000000..61c7a9431de --- /dev/null +++ b/pkg/client/fake_persistent_volume_claims.go @@ -0,0 +1,58 @@ +/* +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/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type FakePersistentVolumeClaims struct { + Fake *Fake + Namespace string +} + +func (c *FakePersistentVolumeClaims) List(labels labels.Selector, field fields.Selector) (*api.PersistentVolumeClaimList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-persistentVolumeClaims"}) + return api.Scheme.CopyOrDie(&c.Fake.PersistentVolumeClaimList).(*api.PersistentVolumeClaimList), c.Fake.Err +} + +func (c *FakePersistentVolumeClaims) Get(name string) (*api.PersistentVolumeClaim, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-persistentVolumeClaim", Value: name}) + return api.Scheme.CopyOrDie(&c.Fake.PersistentVolumeClaim).(*api.PersistentVolumeClaim), nil +} + +func (c *FakePersistentVolumeClaims) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-persistentVolumeClaim", Value: name}) + return nil +} + +func (c *FakePersistentVolumeClaims) Create(persistentvolumeclaim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-persistentVolumeClaim"}) + return &api.PersistentVolumeClaim{}, nil +} + +func (c *FakePersistentVolumeClaims) Update(persistentvolumeclaim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-persistentVolumeClaim", Value: persistentvolumeclaim.Name}) + return &api.PersistentVolumeClaim{}, nil +} + +func (c *FakePersistentVolumeClaims) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.Fake.Watch, c.Fake.Err +} diff --git a/pkg/client/fake_persistent_volumes.go b/pkg/client/fake_persistent_volumes.go new file mode 100644 index 00000000000..e17798b02a3 --- /dev/null +++ b/pkg/client/fake_persistent_volumes.go @@ -0,0 +1,58 @@ +/* +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/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type FakePersistentVolumes struct { + Fake *Fake +} + +func (c *FakePersistentVolumes) List(labels labels.Selector, field fields.Selector) (*api.PersistentVolumeList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-persistentVolumes"}) + return api.Scheme.CopyOrDie(&c.Fake.PersistentVolumesList).(*api.PersistentVolumeList), nil +} + +func (c *FakePersistentVolumes) Get(name string) (*api.PersistentVolume, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-persistentVolume", Value: name}) + return api.Scheme.CopyOrDie(&c.Fake.PersistentVolume).(*api.PersistentVolume), nil +} + +func (c *FakePersistentVolumes) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-persistentVolume", Value: name}) + return nil +} + +func (c *FakePersistentVolumes) Create(persistentvolume *api.PersistentVolume) (*api.PersistentVolume, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-persistentVolume"}) + return &api.PersistentVolume{}, nil +} + +func (c *FakePersistentVolumes) Update(persistentvolume *api.PersistentVolume) (*api.PersistentVolume, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-persistentVolume", Value: persistentvolume.Name}) + return &api.PersistentVolume{}, nil +} + +func (c *FakePersistentVolumes) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-persistentVolumes", Value: resourceVersion}) + return c.Fake.Watch, c.Fake.Err +} diff --git a/pkg/client/persistentvolumeclaim.go b/pkg/client/persistentvolumeclaim.go new file mode 100644 index 00000000000..e3f19189f23 --- /dev/null +++ b/pkg/client/persistentvolumeclaim.go @@ -0,0 +1,103 @@ +/* +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 ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// PersistentVolumeClaimsNamespacer has methods to work with PersistentVolumeClaim resources in a namespace +type PersistentVolumeClaimsNamespacer interface { + PersistentVolumeClaims(namespace string) PersistentVolumeClaimInterface +} + +// PersistentVolumeClaimInterface has methods to work with PersistentVolumeClaim resources. +type PersistentVolumeClaimInterface interface { + List(label labels.Selector, field fields.Selector) (*api.PersistentVolumeClaimList, error) + Get(name string) (*api.PersistentVolumeClaim, error) + Create(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) + Update(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) + Delete(name string) error + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) +} + +// persistentVolumeClaims implements PersistentVolumeClaimsNamespacer interface +type persistentVolumeClaims struct { + client *Client + namespace string +} + +// newPersistentVolumeClaims returns a PodsClient +func newPersistentVolumeClaims(c *Client, namespace string) *persistentVolumeClaims { + return &persistentVolumeClaims{c, namespace} +} + +func (c *persistentVolumeClaims) List(label labels.Selector, field fields.Selector) (result *api.PersistentVolumeClaimList, err error) { + result = &api.PersistentVolumeClaimList{} + + err = c.client.Get(). + Namespace(c.namespace). + Resource("persistentVolumeClaims"). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.client.APIVersion()), field). + Do(). + Into(result) + + return result, err +} + +func (c *persistentVolumeClaims) Get(name string) (result *api.PersistentVolumeClaim, err error) { + result = &api.PersistentVolumeClaim{} + err = c.client.Get().Namespace(c.namespace).Resource("persistentVolumeClaims").Name(name).Do().Into(result) + return +} + +func (c *persistentVolumeClaims) Create(claim *api.PersistentVolumeClaim) (result *api.PersistentVolumeClaim, err error) { + result = &api.PersistentVolumeClaim{} + err = c.client.Post().Namespace(c.namespace).Resource("persistentVolumeClaims").Body(claim).Do().Into(result) + return +} + +func (c *persistentVolumeClaims) Update(claim *api.PersistentVolumeClaim) (result *api.PersistentVolumeClaim, err error) { + result = &api.PersistentVolumeClaim{} + if len(claim.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", claim) + return + } + err = c.client.Put().Namespace(c.namespace).Resource("persistentVolumeClaims").Name(claim.Name).Body(claim).Do().Into(result) + return +} + +func (c *persistentVolumeClaims) Delete(name string) error { + return c.client.Delete().Namespace(c.namespace).Resource("persistentVolumeClaims").Name(name).Do().Error() +} + +func (c *persistentVolumeClaims) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.client.Get(). + Prefix("watch"). + Namespace(c.namespace). + Resource("persistentVolumeClaims"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.client.APIVersion()), field). + Watch() +} diff --git a/pkg/client/persistentvolumes.go b/pkg/client/persistentvolumes.go new file mode 100644 index 00000000000..352ff0776c9 --- /dev/null +++ b/pkg/client/persistentvolumes.go @@ -0,0 +1,97 @@ +/* +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 ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type PersistentVolumesInterface interface { + PersistentVolumes() PersistentVolumeInterface +} + +// PersistentVolumeInterface has methods to work with PersistentVolume resources. +type PersistentVolumeInterface interface { + List(label labels.Selector, field fields.Selector) (*api.PersistentVolumeList, error) + Get(name string) (*api.PersistentVolume, error) + Create(volume *api.PersistentVolume) (*api.PersistentVolume, error) + Update(volume *api.PersistentVolume) (*api.PersistentVolume, error) + Delete(name string) error + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) +} + +// persistentVolumes implements PersistentVolumesInterface +type persistentVolumes struct { + client *Client +} + +func newPersistentVolumes(c *Client) *persistentVolumes { + return &persistentVolumes{c} +} + +func (c *persistentVolumes) List(label labels.Selector, field fields.Selector) (result *api.PersistentVolumeList, err error) { + result = &api.PersistentVolumeList{} + err = c.client.Get(). + Resource("persistentVolumes"). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.client.APIVersion()), field). + Do(). + Into(result) + + return result, err +} + +func (c *persistentVolumes) Get(name string) (result *api.PersistentVolume, err error) { + result = &api.PersistentVolume{} + err = c.client.Get().Resource("persistentVolumes").Name(name).Do().Into(result) + return +} + +func (c *persistentVolumes) Create(volume *api.PersistentVolume) (result *api.PersistentVolume, err error) { + result = &api.PersistentVolume{} + err = c.client.Post().Resource("persistentVolumes").Body(volume).Do().Into(result) + return +} + +func (c *persistentVolumes) Update(volume *api.PersistentVolume) (result *api.PersistentVolume, err error) { + result = &api.PersistentVolume{} + if len(volume.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", volume) + return + } + err = c.client.Put().Resource("persistentVolumes").Name(volume.Name).Body(volume).Do().Into(result) + return +} + +func (c *persistentVolumes) Delete(name string) error { + return c.client.Delete().Resource("persistentVolumes").Name(name).Do().Error() +} + +func (c *persistentVolumes) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.client.Get(). + Prefix("watch"). + Resource("persistentVolumes"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.client.APIVersion()), field). + Watch() +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 5c5de28d51e..8b5173f3c3f 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -68,6 +68,10 @@ func DescriberFor(kind string, c *client.Client) (Describer, bool) { return &ReplicationControllerDescriber{c}, true case "Service": return &ServiceDescriber{c}, true + case "PersistentVolume": + return &PersistentVolumeDescriber{c}, true + case "PersistentVolumeClaim": + return &PersistentVolumeClaimDescriber{c}, true case "Minion", "Node": return &NodeDescriber{c}, true case "LimitRange": @@ -269,6 +273,49 @@ func describePod(pod *api.Pod, rcs []api.ReplicationController, events *api.Even }) } +type PersistentVolumeDescriber struct { + client.Interface +} + +func (d *PersistentVolumeDescriber) Describe(namespace, name string) (string, error) { + c := d.PersistentVolumes() + + pv, err := c.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", pv.Name) + fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pv.Labels)) + fmt.Fprintf(out, "Status:\t%d\n", pv.Status.Phase) + fmt.Fprintf(out, "Claim:\t%d\n", pv.Spec.ClaimRef.UID) + + return nil + }) +} + +type PersistentVolumeClaimDescriber struct { + client.Interface +} + +func (d *PersistentVolumeClaimDescriber) Describe(namespace, name string) (string, error) { + c := d.PersistentVolumeClaims(namespace) + + psd, err := c.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", psd.Name) + fmt.Fprintf(out, "Status:\t%d\n", psd.Status.Phase) + fmt.Fprintf(out, "Volume:\t%d\n", psd.Status.VolumeRef.UID) + + return nil + }) +} + func describeContainers(containers []api.ContainerStatus, out io.Writer) { for _, container := range containers { fmt.Fprintf(out, " %v:\n", container.Name) diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 166f4876485..0a61a55feab 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -107,6 +107,8 @@ func expandResourceShortcut(resource string) string { "ev": "events", "limits": "limitRanges", "quota": "resourceQuotas", + "pv": "persistentVolumes", + "pvc": "persistentVolumeClaims", } if expanded, ok := shortForms[resource]; ok { return expanded diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 787dc3de874..757ed5dafea 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -32,6 +32,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "github.com/docker/docker/pkg/units" "github.com/ghodss/yaml" "github.com/golang/glog" @@ -237,6 +238,8 @@ var limitRangeColumns = []string{"NAME"} var resourceQuotaColumns = []string{"NAME"} var namespaceColumns = []string{"NAME", "LABELS", "STATUS"} var secretColumns = []string{"NAME", "DATA"} +var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"} +var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"} // addDefaultHandlers adds print handlers for default Kubernetes types. func (h *HumanReadablePrinter) addDefaultHandlers() { @@ -261,6 +264,10 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(namespaceColumns, printNamespaceList) h.Handler(secretColumns, printSecret) h.Handler(secretColumns, printSecretList) + h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaim) + h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList) + h.Handler(persistentVolumeColumns, printPersistentVolume) + h.Handler(persistentVolumeColumns, printPersistentVolumeList) } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { @@ -506,6 +513,50 @@ func printNodeList(list *api.NodeList, w io.Writer) error { return nil } +func printPersistentVolume(pv *api.PersistentVolume, w io.Writer) error { + claimRefUID := "" + if pv.Spec.ClaimRef != nil { + claimRefUID += pv.Spec.ClaimRef.Name + claimRefUID += " / " + claimRefUID += string(pv.Spec.ClaimRef.UID) + } + + modesStr := volume.GetAccessModesAsString(pv.Spec.AccessModes) + + aQty := pv.Spec.Capacity[api.ResourceStorage] + aSize := aQty.Value() + + _, err := fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\n", pv.Name, pv.Labels, aSize, modesStr, pv.Status.Phase, claimRefUID) + return err +} + +func printPersistentVolumeList(list *api.PersistentVolumeList, w io.Writer) error { + for _, pv := range list.Items { + if err := printPersistentVolume(&pv, w); err != nil { + return err + } + } + return nil +} + +func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer) error { + volumeRefUID := "" + if pvc.Status.VolumeRef != nil { + volumeRefUID = string(pvc.Status.VolumeRef.UID) + } + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pvc.Name, pvc.Labels, pvc.Status.Phase, volumeRefUID) + return err +} + +func printPersistentVolumeClaimList(list *api.PersistentVolumeClaimList, w io.Writer) error { + for _, psd := range list.Items { + if err := printPersistentVolumeClaim(&psd, w); err != nil { + return err + } + } + return nil +} + func printStatus(status *api.Status, w io.Writer) error { _, err := fmt.Fprintf(w, "%v\n", status.Status) return err diff --git a/pkg/master/master.go b/pkg/master/master.go index ebcc9b6f9c1..9a92bf48d82 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -52,6 +52,8 @@ import ( nodeetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" namespaceetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace/etcd" + pvetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/persistentvolume/etcd" + 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" resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd" @@ -363,6 +365,8 @@ func (m *Master) init(c *Config) { resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper) secretRegistry := secret.NewEtcdRegistry(c.EtcdHelper) + persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewStorage(c.EtcdHelper) + persistentVolumeClaimStorage := pvcetcd.NewStorage(c.EtcdHelper) namespaceStorage, namespaceStatusStorage, namespaceFinalizeStorage := namespaceetcd.NewStorage(c.EtcdHelper) m.namespaceRegistry = namespace.NewRegistry(namespaceStorage) @@ -393,13 +397,16 @@ func (m *Master) init(c *Config) { "nodes": nodeStorage, "events": event.NewStorage(eventRegistry), - "limitRanges": limitrange.NewStorage(limitRangeRegistry), - "resourceQuotas": resourceQuotaStorage, - "resourceQuotas/status": resourceQuotaStatusStorage, - "namespaces": namespaceStorage, - "namespaces/status": namespaceStatusStorage, - "namespaces/finalize": namespaceFinalizeStorage, - "secrets": secret.NewStorage(secretRegistry), + "limitRanges": limitrange.NewStorage(limitRangeRegistry), + "resourceQuotas": resourceQuotaStorage, + "resourceQuotas/status": resourceQuotaStatusStorage, + "namespaces": namespaceStorage, + "namespaces/status": namespaceStatusStorage, + "namespaces/finalize": namespaceFinalizeStorage, + "secrets": secret.NewStorage(secretRegistry), + "persistentVolumes": persistentVolumeStorage, + "persistentVolumes/status": persistentVolumeStatusStorage, + "persistentVolumeClaims": persistentVolumeClaimStorage, } apiVersions := []string{"v1beta1", "v1beta2"} diff --git a/pkg/namespace/namespace_controller.go b/pkg/namespace/namespace_controller.go index 2ecdbf1a028..667251ce8cd 100644 --- a/pkg/namespace/namespace_controller.go +++ b/pkg/namespace/namespace_controller.go @@ -133,6 +133,10 @@ func deleteAllContent(kubeClient client.Interface, namespace string) (err error) if err != nil { return err } + err = deletePersistentVolumeClaims(kubeClient, namespace) + if err != nil { + return err + } err = deleteLimitRanges(kubeClient, namespace) if err != nil { return err @@ -292,3 +296,17 @@ func deleteSecrets(kubeClient client.Interface, ns string) error { } return nil } + +func deletePersistentVolumeClaims(kubeClient client.Interface, ns string) error { + items, err := kubeClient.PersistentVolumeClaims(ns).List(labels.Everything(), fields.Everything()) + if err != nil { + return err + } + for i := range items.Items { + err := kubeClient.PersistentVolumeClaims(ns).Delete(items.Items[i].Name) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/registry/persistentvolume/doc.go b/pkg/registry/persistentvolume/doc.go new file mode 100644 index 00000000000..738b60359fb --- /dev/null +++ b/pkg/registry/persistentvolume/doc.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 persistentvolume diff --git a/pkg/registry/persistentvolume/etcd/etcd.go b/pkg/registry/persistentvolume/etcd/etcd.go new file mode 100644 index 00000000000..69baf3b34db --- /dev/null +++ b/pkg/registry/persistentvolume/etcd/etcd.go @@ -0,0 +1,80 @@ +/* +Copyright 2015 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/persistentvolume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// rest implements a RESTStorage for persistentvolumes against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against PersistentVolume objects. +func NewStorage(h tools.EtcdHelper) (*REST, *StatusREST) { + prefix := "/registry/persistentvolumes" + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.PersistentVolume{} }, + NewListFunc: func() runtime.Object { return &api.PersistentVolumeList{} }, + KeyRootFunc: func(ctx api.Context) string { + return prefix + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return prefix + "/" + name, nil + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.PersistentVolume).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return persistentvolume.MatchPersistentVolumes(label, field) + }, + EndpointName: "persistentvolume", + + Helper: h, + } + + store.CreateStrategy = persistentvolume.Strategy + store.UpdateStrategy = persistentvolume.Strategy + store.ReturnDeletedObject = true + + statusStore := *store + statusStore.UpdateStrategy = persistentvolume.StatusStrategy + + return &REST{store}, &StatusREST{store: &statusStore} +} + +// StatusREST implements the REST endpoint for changing the status of a persistentvolume. +type StatusREST struct { + store *etcdgeneric.Etcd +} + +func (r *StatusREST) New() runtime.Object { + return &api.PersistentVolume{} +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) +} diff --git a/pkg/registry/persistentvolume/etcd/etcd_test.go b/pkg/registry/persistentvolume/etcd/etcd_test.go new file mode 100644 index 00000000000..8a839222058 --- /dev/null +++ b/pkg/registry/persistentvolume/etcd/etcd_test.go @@ -0,0 +1,342 @@ +/* +Copyright 2015 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/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/coreos/go-etcd/etcd" +) + +type testRegistry struct { + *registrytest.GenericRegistry +} + +func newStorage(t *testing.T) (*REST, *StatusREST, *tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.NewEtcdHelper(fakeEtcdClient, latest.Codec) + storage, statusStorage := NewStorage(helper) + return storage, statusStorage, fakeEtcdClient, helper +} + +func validNewPersistentVolume(name string) *api.PersistentVolume { + pv := &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: name, + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("10G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + }, + }, + } + return pv +} + +func validChangedPersistentVolume() *api.PersistentVolume { + pv := validNewPersistentVolume("foo") + pv.ResourceVersion = "1" + return pv +} + +func TestCreate(t *testing.T) { + storage, _, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + pv := validNewPersistentVolume("foo") + pv.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + pv, + // invalid + &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{Name: "*BadName!"}, + }, + ) +} + +func TestDelete(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + + pv := validChangedPersistentVolume() + key, _ := storage.KeyFunc(ctx, pv.Name) + createFn := func() runtime.Object { + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, pv), + ModifiedIndex: 1, + }, + }, + } + return pv + } + gracefulSetFn := func() bool { + if fakeEtcdClient.Data[key].R.Node == nil { + return false + } + return fakeEtcdClient.Data[key].R.Node.TTL == 30 + } + test.TestDeleteNoGraceful(createFn, gracefulSetFn) +} + +func TestEtcdListPersistentVolumes(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + key := storage.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, validNewPersistentVolume("foo")), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, validNewPersistentVolume("bar")), + }, + }, + }, + }, + E: nil, + } + + pvObj, err := storage.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + pvs := pvObj.(*api.PersistentVolumeList) + + if len(pvs.Items) != 2 || pvs.Items[0].Name != "foo" || pvs.Items[1].Name != "bar" { + t.Errorf("Unexpected persistentVolume list: %#v", pvs) + } +} + +func TestEtcdGetPersistentVolumes(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + persistentVolume := validNewPersistentVolume("foo") + name := persistentVolume.Name + key, _ := storage.KeyFunc(ctx, name) + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, persistentVolume), 0) + + response, err := fakeClient.Get(key, false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var persistentVolumeOut api.PersistentVolume + err = latest.Codec.DecodeInto([]byte(response.Node.Value), &persistentVolumeOut) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + obj, err := storage.Get(ctx, name) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + got := obj.(*api.PersistentVolume) + + persistentVolume.ObjectMeta.ResourceVersion = got.ObjectMeta.ResourceVersion + if e, a := persistentVolume, got; !api.Semantic.DeepEqual(*e, *a) { + t.Errorf("Unexpected persistentVolume: %#v, expected %#v", e, a) + } +} + +func TestListEmptyPersistentVolumesList(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := storage.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: fakeClient.NewError(tools.EtcdErrorCodeNotFound), + } + + persistentVolume, err := storage.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(persistentVolume.(*api.PersistentVolumeList).Items) != 0 { + t.Errorf("Unexpected non-zero pod list: %#v", persistentVolume) + } + if persistentVolume.(*api.PersistentVolumeList).ResourceVersion != "1" { + t.Errorf("Unexpected resource version: %#v", persistentVolume) + } +} + +func TestListPersistentVolumesList(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := storage.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + } + + persistentVolumeObj, err := storage.List(ctx, labels.Everything(), fields.Everything()) + persistentVolumeList := persistentVolumeObj.(*api.PersistentVolumeList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(persistentVolumeList.Items) != 2 { + t.Errorf("Unexpected persistentVolume list: %#v", persistentVolumeList) + } + if persistentVolumeList.Items[0].Name != "foo" { + t.Errorf("Unexpected persistentVolume: %#v", persistentVolumeList.Items[0]) + } + if persistentVolumeList.Items[1].Name != "bar" { + t.Errorf("Unexpected persistentVolume: %#v", persistentVolumeList.Items[1]) + } +} + +func TestPersistentVolumesDecode(t *testing.T) { + storage, _, _, _ := newStorage(t) + expected := validNewPersistentVolume("foo") + body, err := latest.Codec.Encode(expected) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual := storage.New() + if err := latest.Codec.DecodeInto(body, actual); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !api.Semantic.DeepEqual(expected, actual) { + t.Errorf("mismatch: %s", util.ObjectDiff(expected, actual)) + } +} + +func TestEtcdUpdatePersistentVolumes(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + persistentVolume := validChangedPersistentVolume() + + key, _ := storage.KeyFunc(ctx, "foo") + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, validNewPersistentVolume("foo")), 0) + + _, _, err := storage.Update(ctx, persistentVolume) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + response, err := fakeClient.Get(key, false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var persistentVolumeOut api.PersistentVolume + err = latest.Codec.DecodeInto([]byte(response.Node.Value), &persistentVolumeOut) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + persistentVolume.ObjectMeta.ResourceVersion = persistentVolumeOut.ObjectMeta.ResourceVersion + if !api.Semantic.DeepEqual(persistentVolume, &persistentVolumeOut) { + t.Errorf("Unexpected persistentVolume: %#v, expected %#v", &persistentVolumeOut, persistentVolume) + } +} + +func TestDeletePersistentVolumes(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeClient, _ := newStorage(t) + persistentVolume := validNewPersistentVolume("foo") + name := persistentVolume.Name + key, _ := storage.KeyFunc(ctx, name) + fakeClient.ChangeIndex = 1 + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, persistentVolume), + ModifiedIndex: 1, + CreatedIndex: 1, + }, + }, + } + _, err := storage.Delete(ctx, name, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestEtcdUpdateStatus(t *testing.T) { + storage, statusStorage, fakeClient, helper := newStorage(t) + ctx := api.NewDefaultContext() + fakeClient.TestIndex = true + + key, _ := storage.KeyFunc(ctx, "foo") + pvStart := validNewPersistentVolume("foo") + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, pvStart), 1) + + pvIn := &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + ResourceVersion: "1", + }, + Status: api.PersistentVolumeStatus{ + Phase: api.VolumeBound, + }, + } + + expected := *pvStart + expected.ResourceVersion = "2" + expected.Labels = pvIn.Labels + expected.Status = pvIn.Status + + _, _, err := statusStorage.Update(ctx, pvIn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + var pvOut api.PersistentVolume + if err := helper.ExtractObj(key, &pvOut, false); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !api.Semantic.DeepEqual(expected, pvOut) { + t.Errorf("unexpected object: %s", util.ObjectDiff(expected, pvOut)) + } +} diff --git a/pkg/registry/persistentvolume/registry.go b/pkg/registry/persistentvolume/registry.go new file mode 100644 index 00000000000..76b668a9895 --- /dev/null +++ b/pkg/registry/persistentvolume/registry.go @@ -0,0 +1,87 @@ +/* +Copyright 2015 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 persistentvolume + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Registry is an interface implemented by things that know how to store PersistentVolume objects. +type Registry interface { + // ListPersistentVolumes obtains a list of persistentVolumes having labels which match selector. + ListPersistentVolumes(ctx api.Context, selector labels.Selector) (*api.PersistentVolumeList, error) + // Watch for new/changed/deleted persistentVolumes + WatchPersistentVolumes(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific persistentVolume + GetPersistentVolume(ctx api.Context, persistentVolumeID string) (*api.PersistentVolume, error) + // Create a persistentVolume based on a specification. + CreatePersistentVolume(ctx api.Context, persistentVolume *api.PersistentVolume) error + // Update an existing persistentVolume + UpdatePersistentVolume(ctx api.Context, persistentVolume *api.PersistentVolume) error + // Delete an existing persistentVolume + DeletePersistentVolume(ctx api.Context, persistentVolumeID 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) ListPersistentVolumes(ctx api.Context, label labels.Selector) (*api.PersistentVolumeList, error) { + obj, err := s.List(ctx, label, fields.Everything()) + if err != nil { + return nil, err + } + return obj.(*api.PersistentVolumeList), nil +} + +func (s *storage) WatchPersistentVolumes(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetPersistentVolume(ctx api.Context, persistentVolumeID string) (*api.PersistentVolume, error) { + obj, err := s.Get(ctx, persistentVolumeID) + if err != nil { + return nil, err + } + return obj.(*api.PersistentVolume), nil +} + +func (s *storage) CreatePersistentVolume(ctx api.Context, persistentVolume *api.PersistentVolume) error { + _, err := s.Create(ctx, persistentVolume) + return err +} + +func (s *storage) UpdatePersistentVolume(ctx api.Context, persistentVolume *api.PersistentVolume) error { + _, _, err := s.Update(ctx, persistentVolume) + return err +} + +func (s *storage) DeletePersistentVolume(ctx api.Context, persistentVolumeID string) error { + _, err := s.Delete(ctx, persistentVolumeID, nil) + return err +} diff --git a/pkg/registry/persistentvolume/rest.go b/pkg/registry/persistentvolume/rest.go new file mode 100644 index 00000000000..8de5c44f4b4 --- /dev/null +++ b/pkg/registry/persistentvolume/rest.go @@ -0,0 +1,109 @@ +/* +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 persistentvolume + +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" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +// persistentvolumeStrategy implements behavior for PersistentVolume objects +type persistentvolumeStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating PersistentVolume +// objects via the REST API. +var Strategy = persistentvolumeStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is false for persistentvolumes. +func (persistentvolumeStrategy) NamespaceScoped() bool { + return false +} + +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (persistentvolumeStrategy) PrepareForCreate(obj runtime.Object) { + pv := obj.(*api.PersistentVolume) + pv.Status = api.PersistentVolumeStatus{} +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (persistentvolumeStrategy) PrepareForUpdate(obj, old runtime.Object) { + newPv := obj.(*api.PersistentVolume) + oldPv := obj.(*api.PersistentVolume) + newPv.Status = oldPv.Status +} + +// Validate validates a new persistentvolume. +func (persistentvolumeStrategy) Validate(obj runtime.Object) fielderrors.ValidationErrorList { + persistentvolume := obj.(*api.PersistentVolume) + return validation.ValidatePersistentVolume(persistentvolume) +} + +// AllowCreateOnUpdate is false for persistentvolumes. +func (persistentvolumeStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (persistentvolumeStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidatePersistentVolumeUpdate(obj.(*api.PersistentVolume), old.(*api.PersistentVolume)) +} + +type persistentvolumeStatusStrategy struct { + persistentvolumeStrategy +} + +var StatusStrategy = persistentvolumeStatusStrategy{Strategy} + +func (persistentvolumeStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + newPv := obj.(*api.PersistentVolume) + oldPv := obj.(*api.PersistentVolume) + newPv.Spec = oldPv.Spec +} + +func (persistentvolumeStatusStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidatePersistentVolumeStatusUpdate(obj.(*api.PersistentVolume), old.(*api.PersistentVolume)) +} + +// MatchPersistentVolume returns a generic matcher for a given label and field selector. +func MatchPersistentVolumes(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + persistentvolumeObj, ok := obj.(*api.PersistentVolume) + if !ok { + return false, fmt.Errorf("not a persistentvolume") + } + fields := PersistentVolumeToSelectableFields(persistentvolumeObj) + return label.Matches(labels.Set(persistentvolumeObj.Labels)) && field.Matches(fields), nil + }) +} + +// PersistentVolumeToSelectableFields returns a label set that represents the object +// TODO: fields are not labels, and the validation rules for them do not apply. +func PersistentVolumeToSelectableFields(persistentvolume *api.PersistentVolume) labels.Set { + return labels.Set{ + "name": persistentvolume.Name, + } +} diff --git a/pkg/registry/persistentvolumeclaim/doc.go b/pkg/registry/persistentvolumeclaim/doc.go new file mode 100644 index 00000000000..9d0bd32c81c --- /dev/null +++ b/pkg/registry/persistentvolumeclaim/doc.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 persistentvolumeclaim diff --git a/pkg/registry/persistentvolumeclaim/etcd/etcd.go b/pkg/registry/persistentvolumeclaim/etcd/etcd.go new file mode 100644 index 00000000000..ada9ce88d89 --- /dev/null +++ b/pkg/registry/persistentvolumeclaim/etcd/etcd.go @@ -0,0 +1,63 @@ +/* +Copyright 2015 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/persistentvolumeclaim" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// rest implements a RESTStorage for persistentvolumeclaims against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against PersistentVolumeClaim objects. +func NewStorage(h tools.EtcdHelper) *REST { + prefix := "/registry/persistentvolumeclaims" + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.PersistentVolumeClaim{} }, + NewListFunc: func() runtime.Object { return &api.PersistentVolumeClaimList{} }, + 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.PersistentVolumeClaim).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return persistentvolumeclaim.MatchPersistentVolumeClaim(label, field) + }, + EndpointName: "persistentvolumeclaims", + + Helper: h, + } + + store.CreateStrategy = persistentvolumeclaim.Strategy + store.UpdateStrategy = persistentvolumeclaim.Strategy + store.ReturnDeletedObject = true + + return &REST{store} +} diff --git a/pkg/registry/persistentvolumeclaim/etcd/etcd_test.go b/pkg/registry/persistentvolumeclaim/etcd/etcd_test.go new file mode 100644 index 00000000000..d9af475edf1 --- /dev/null +++ b/pkg/registry/persistentvolumeclaim/etcd/etcd_test.go @@ -0,0 +1,334 @@ +/* +Copyright 2015 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/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/coreos/go-etcd/etcd" +) + +type testRegistry struct { + *registrytest.GenericRegistry +} + +func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.NewEtcdHelper(fakeEtcdClient, latest.Codec) + storage := NewStorage(helper) + return storage, fakeEtcdClient, helper +} + +func validNewPersistentVolumeClaim(name, ns string) *api.PersistentVolumeClaim { + pv := &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("10G"), + }, + }, + }, + } + return pv +} + +func validChangedPersistentVolumeClaim() *api.PersistentVolumeClaim { + pv := validNewPersistentVolumeClaim("foo", api.NamespaceDefault) + pv.ResourceVersion = "1" + return pv +} + +func TestCreate(t *testing.T) { + + registry, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, registry, fakeEtcdClient.SetError) + pv := validNewPersistentVolumeClaim("foo", api.NamespaceDefault) + pv.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + pv, + // invalid + &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{Name: "*BadName!"}, + }, + ) +} + +func TestDelete2(t *testing.T) { + ctx := api.NewDefaultContext() + storage, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + + pv := validChangedPersistentVolumeClaim() + key, _ := storage.KeyFunc(ctx, pv.Name) + createFn := func() runtime.Object { + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, pv), + ModifiedIndex: 1, + }, + }, + } + return pv + } + gracefulSetFn := func() bool { + if fakeEtcdClient.Data[key].R.Node == nil { + return false + } + return fakeEtcdClient.Data[key].R.Node.TTL == 30 + } + test.TestDeleteNoGraceful(createFn, gracefulSetFn) +} + +func TestDelete(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, registry, fakeEtcdClient.SetError) + + pvc := validChangedPersistentVolumeClaim() + key, _ := registry.KeyFunc(ctx, pvc.Name) + createFn := func() runtime.Object { + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, pvc), + ModifiedIndex: 1, + }, + }, + } + return pvc + } + gracefulSetFn := func() bool { + if fakeEtcdClient.Data[key].R.Node == nil { + return false + } + return fakeEtcdClient.Data[key].R.Node.TTL == 30 + } + test.TestDeleteNoGraceful(createFn, gracefulSetFn) +} + +func TestEtcdListPersistentVolumeClaims(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + key := registry.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, validNewPersistentVolumeClaim("foo", api.NamespaceDefault)), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, validNewPersistentVolumeClaim("bar", api.NamespaceDefault)), + }, + }, + }, + }, + E: nil, + } + + pvObj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + pvs := pvObj.(*api.PersistentVolumeClaimList) + + if len(pvs.Items) != 2 || pvs.Items[0].Name != "foo" || pvs.Items[1].Name != "bar" { + t.Errorf("Unexpected persistentVolume list: %#v", pvs) + } +} + +func TestEtcdGetPersistentVolumeClaims(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + persistentVolume := validNewPersistentVolumeClaim("foo", api.NamespaceDefault) + name := persistentVolume.Name + key, _ := registry.KeyFunc(ctx, name) + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, persistentVolume), 0) + + response, err := fakeClient.Get(key, false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var persistentVolumeOut api.PersistentVolumeClaim + err = latest.Codec.DecodeInto([]byte(response.Node.Value), &persistentVolumeOut) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + obj, err := registry.Get(ctx, name) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + got := obj.(*api.PersistentVolumeClaim) + + persistentVolume.ObjectMeta.ResourceVersion = got.ObjectMeta.ResourceVersion + if e, a := persistentVolume, got; !api.Semantic.DeepEqual(*e, *a) { + t.Errorf("Unexpected persistentVolume: %#v, expected %#v", e, a) + } +} + +func TestListEmptyPersistentVolumeClaimsList(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := registry.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: fakeClient.NewError(tools.EtcdErrorCodeNotFound), + } + + persistentVolume, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(persistentVolume.(*api.PersistentVolumeClaimList).Items) != 0 { + t.Errorf("Unexpected non-zero pod list: %#v", persistentVolume) + } + if persistentVolume.(*api.PersistentVolumeClaimList).ResourceVersion != "1" { + t.Errorf("Unexpected resource version: %#v", persistentVolume) + } +} + +func TestListPersistentVolumeClaimsList(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := registry.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + } + + persistentVolumeObj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + persistentVolumeList := persistentVolumeObj.(*api.PersistentVolumeClaimList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(persistentVolumeList.Items) != 2 { + t.Errorf("Unexpected persistentVolume list: %#v", persistentVolumeList) + } + if persistentVolumeList.Items[0].Name != "foo" { + t.Errorf("Unexpected persistentVolume: %#v", persistentVolumeList.Items[0]) + } + if persistentVolumeList.Items[1].Name != "bar" { + t.Errorf("Unexpected persistentVolume: %#v", persistentVolumeList.Items[1]) + } +} + +func TestPersistentVolumeClaimsDecode(t *testing.T) { + registry, _, _ := newStorage(t) + expected := validNewPersistentVolumeClaim("foo", api.NamespaceDefault) + body, err := latest.Codec.Encode(expected) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual := registry.New() + if err := latest.Codec.DecodeInto(body, actual); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !api.Semantic.DeepEqual(expected, actual) { + t.Errorf("mismatch: %s", util.ObjectDiff(expected, actual)) + } +} + +func TestEtcdUpdatePersistentVolumeClaims(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + persistentVolume := validChangedPersistentVolumeClaim() + + key, _ := registry.KeyFunc(ctx, "foo") + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, validNewPersistentVolumeClaim("foo", api.NamespaceDefault)), 0) + + _, _, err := registry.Update(ctx, persistentVolume) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + response, err := fakeClient.Get(key, false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var persistentVolumeOut api.PersistentVolumeClaim + err = latest.Codec.DecodeInto([]byte(response.Node.Value), &persistentVolumeOut) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + persistentVolume.ObjectMeta.ResourceVersion = persistentVolumeOut.ObjectMeta.ResourceVersion + if !api.Semantic.DeepEqual(persistentVolume, &persistentVolumeOut) { + t.Errorf("Unexpected persistentVolume: %#v, expected %#v", &persistentVolumeOut, persistentVolume) + } +} + +func TestDeletePersistentVolumeClaims(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + persistentVolume := validNewPersistentVolumeClaim("foo", api.NamespaceDefault) + name := persistentVolume.Name + key, _ := registry.KeyFunc(ctx, name) + fakeClient.ChangeIndex = 1 + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, persistentVolume), + ModifiedIndex: 1, + CreatedIndex: 1, + }, + }, + } + _, err := registry.Delete(ctx, name, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/pkg/registry/persistentvolumeclaim/registry.go b/pkg/registry/persistentvolumeclaim/registry.go new file mode 100644 index 00000000000..2fe2218e528 --- /dev/null +++ b/pkg/registry/persistentvolumeclaim/registry.go @@ -0,0 +1,87 @@ +/* +Copyright 2015 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 persistentvolumeclaim + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Registry is an interface implemented by things that know how to store PersistentVolumeClaim objects. +type Registry interface { + // ListPersistentVolumeClaims obtains a list of PVCs having labels which match selector. + ListPersistentVolumeClaims(ctx api.Context, selector labels.Selector) (*api.PersistentVolumeClaimList, error) + // Watch for new/changed/deleted PVCs + WatchPersistentVolumeClaims(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific PVC + GetPersistentVolumeClaim(ctx api.Context, pvcID string) (*api.PersistentVolumeClaim, error) + // Create a PVC based on a specification. + CreatePersistentVolumeClaim(ctx api.Context, pvc *api.PersistentVolumeClaim) error + // Update an existing PVC + UpdatePersistentVolumeClaim(ctx api.Context, pvc *api.PersistentVolumeClaim) error + // Delete an existing PVC + DeletePersistentVolumeClaim(ctx api.Context, pvcID 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) ListPersistentVolumeClaims(ctx api.Context, label labels.Selector) (*api.PersistentVolumeClaimList, error) { + obj, err := s.List(ctx, label, fields.Everything()) + if err != nil { + return nil, err + } + return obj.(*api.PersistentVolumeClaimList), nil +} + +func (s *storage) WatchPersistentVolumeClaims(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetPersistentVolumeClaim(ctx api.Context, podID string) (*api.PersistentVolumeClaim, error) { + obj, err := s.Get(ctx, podID) + if err != nil { + return nil, err + } + return obj.(*api.PersistentVolumeClaim), nil +} + +func (s *storage) CreatePersistentVolumeClaim(ctx api.Context, pod *api.PersistentVolumeClaim) error { + _, err := s.Create(ctx, pod) + return err +} + +func (s *storage) UpdatePersistentVolumeClaim(ctx api.Context, pod *api.PersistentVolumeClaim) error { + _, _, err := s.Update(ctx, pod) + return err +} + +func (s *storage) DeletePersistentVolumeClaim(ctx api.Context, podID string) error { + _, err := s.Delete(ctx, podID, nil) + return err +} diff --git a/pkg/registry/persistentvolumeclaim/rest.go b/pkg/registry/persistentvolumeclaim/rest.go new file mode 100644 index 00000000000..82d2f1300a6 --- /dev/null +++ b/pkg/registry/persistentvolumeclaim/rest.go @@ -0,0 +1,109 @@ +/* +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 persistentvolumeclaim + +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" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +// persistentvolumeclaimStrategy implements behavior for PersistentVolumeClaim objects +type persistentvolumeclaimStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating PersistentVolumeClaim +// objects via the REST API. +var Strategy = persistentvolumeclaimStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is true for persistentvolumeclaims. +func (persistentvolumeclaimStrategy) NamespaceScoped() bool { + return true +} + +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (persistentvolumeclaimStrategy) PrepareForCreate(obj runtime.Object) { + pv := obj.(*api.PersistentVolumeClaim) + pv.Status = api.PersistentVolumeClaimStatus{} +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (persistentvolumeclaimStrategy) PrepareForUpdate(obj, old runtime.Object) { + newPvc := obj.(*api.PersistentVolumeClaim) + oldPvc := obj.(*api.PersistentVolumeClaim) + newPvc.Status = oldPvc.Status +} + +// Validate validates a new persistentvolumeclaim. +func (persistentvolumeclaimStrategy) Validate(obj runtime.Object) fielderrors.ValidationErrorList { + pvc := obj.(*api.PersistentVolumeClaim) + return validation.ValidatePersistentVolumeClaim(pvc) +} + +// AllowCreateOnUpdate is false for persistentvolumeclaims. +func (persistentvolumeclaimStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (persistentvolumeclaimStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidatePersistentVolumeClaimUpdate(obj.(*api.PersistentVolumeClaim), old.(*api.PersistentVolumeClaim)) +} + +type persistentvolumeclaimStatusStrategy struct { + persistentvolumeclaimStrategy +} + +var StatusStrategy = persistentvolumeclaimStatusStrategy{Strategy} + +func (persistentvolumeclaimStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + newPvc := obj.(*api.PersistentVolumeClaim) + oldPvc := obj.(*api.PersistentVolumeClaim) + newPvc.Spec = oldPvc.Spec +} + +func (persistentvolumeclaimStatusStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidatePersistentVolumeClaimStatusUpdate(obj.(*api.PersistentVolumeClaim), old.(*api.PersistentVolumeClaim)) +} + +// MatchPersistentVolumeClaim returns a generic matcher for a given label and field selector. +func MatchPersistentVolumeClaim(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + persistentvolumeclaimObj, ok := obj.(*api.PersistentVolumeClaim) + if !ok { + return false, fmt.Errorf("not a persistentvolumeclaim") + } + fields := PersistentVolumeClaimToSelectableFields(persistentvolumeclaimObj) + return label.Matches(labels.Set(persistentvolumeclaimObj.Labels)) && field.Matches(fields), nil + }) +} + +// PersistentVolumeClaimToSelectableFields returns a label set that represents the object +// TODO: fields are not labels, and the validation rules for them do not apply. +func PersistentVolumeClaimToSelectableFields(persistentvolumeclaim *api.PersistentVolumeClaim) labels.Set { + return labels.Set{ + "name": persistentvolumeclaim.Name, + } +} diff --git a/pkg/volume/util.go b/pkg/volume/util.go new file mode 100644 index 00000000000..1e64d4078ee --- /dev/null +++ b/pkg/volume/util.go @@ -0,0 +1,54 @@ +/* +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 volume + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func GetAccessModesAsString(modes []api.AccessModeType) string { + modesAsString := "" + + if contains(modes, api.ReadWriteOnce) { + appendAccessMode(modesAsString, "RWO") + } + if contains(modes, api.ReadOnlyMany) { + appendAccessMode(modesAsString, "ROX") + } + if contains(modes, api.ReadWriteMany) { + appendAccessMode(modesAsString, "RWX") + } + + return modesAsString +} + +func appendAccessMode(modes, mode string) string { + if modes != "" { + modes += "," + } + modes += mode + return modes +} + +func contains(modes []api.AccessModeType, mode api.AccessModeType) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +}