diff --git a/cmd/kubelet/app/plugins.go b/cmd/kubelet/app/plugins.go index d0dd25498dc..f0c1c01b9f0 100644 --- a/cmd/kubelet/app/plugins.go +++ b/cmd/kubelet/app/plugins.go @@ -33,6 +33,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/host_path" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/iscsi" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/nfs" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/persistent_claim" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/secret" //Cloud providers _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" @@ -59,6 +60,7 @@ func ProbeVolumePlugins() []volume.VolumePlugin { allPlugins = append(allPlugins, secret.ProbeVolumePlugins()...) allPlugins = append(allPlugins, iscsi.ProbeVolumePlugins()...) allPlugins = append(allPlugins, glusterfs.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, persistent_claim.ProbeVolumePlugins()...) return allPlugins } diff --git a/pkg/api/types.go b/pkg/api/types.go index bce161e5fce..cc91b969afe 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -203,6 +203,8 @@ type VolumeSource struct { ISCSI *ISCSIVolumeSource `json:"iscsi"` // Glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime Glusterfs *GlusterfsVolumeSource `json:"glusterfs"` + // PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace + PersistentVolumeClaimVolumeSource *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"` } // Similar to VolumeSource but meant for the administrator who creates PVs. @@ -222,6 +224,14 @@ type PersistentVolumeSource struct { Glusterfs *GlusterfsVolumeSource `json:"glusterfs"` } +type PersistentVolumeClaimVolumeSource struct { + // ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume + ClaimName string `json:"claimName,omitempty" description:"the name of the claim in the same namespace to be mounted as a volume"` + // Optional: Defaults to false (read/write). ReadOnly here + // will force the ReadOnly setting in VolumeMounts + ReadOnly bool `json:"readOnly,omitempty"` +} + type PersistentVolume struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 31219131d70..00abab8fbde 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1185,6 +1185,9 @@ func init() { if err := s.Convert(&in.Glusterfs, &out.Glusterfs, 0); err != nil { return err } + if err := s.Convert(&in.PersistentVolumeClaimVolumeSource, &out.PersistentVolumeClaimVolumeSource, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -1212,6 +1215,9 @@ func init() { if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { return err } + if err := s.Convert(&in.PersistentVolumeClaimVolumeSource, &out.PersistentVolumeClaimVolumeSource, 0); err != nil { + return err + } if err := s.Convert(&in.Glusterfs, &out.Glusterfs, 0); err != nil { return err } diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 418f1a87fa4..8ad1fb9ea97 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -119,6 +119,8 @@ type VolumeSource struct { ISCSI *ISCSIVolumeSource `json:"iscsi" description:"iSCSI disk attached to host machine on demand"` // Glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime Glusterfs *GlusterfsVolumeSource `json:"glusterfs" description:"Glusterfs volume that will be mounted on the host machine "` + // PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace + PersistentVolumeClaimVolumeSource *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty" description:"a reference to a PersistentVolumeClaim in the same namespace"` } // Similar to VolumeSource but meant for the administrator who creates PVs. @@ -138,6 +140,14 @@ type PersistentVolumeSource struct { Glusterfs *GlusterfsVolumeSource `json:"glusterfs" description:"Glusterfs volume resource provisioned by an admin"` } +type PersistentVolumeClaimVolumeSource struct { + // ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume + ClaimName string `json:"claimName,omitempty" description:"the name of the claim in the same namespace to be mounted as a volume"` + // Optional: Defaults to false (read/write). ReadOnly here + // will force the ReadOnly setting in VolumeMounts + ReadOnly bool `json:"readOnly,omitempty" description:"mount volume as read-only when true; default false"` +} + type PersistentVolume struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 96b20924107..d29c33904b1 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -1112,6 +1112,9 @@ func init() { if err := s.Convert(&in.Glusterfs, &out.Glusterfs, 0); err != nil { return err } + if err := s.Convert(&in.PersistentVolumeClaimVolumeSource, &out.PersistentVolumeClaimVolumeSource, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -1139,6 +1142,9 @@ func init() { if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { return err } + if err := s.Convert(&in.PersistentVolumeClaimVolumeSource, &out.PersistentVolumeClaimVolumeSource, 0); err != nil { + return err + } if err := s.Convert(&in.Glusterfs, &out.Glusterfs, 0); err != nil { return err } diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 0b944658b6a..a76ea787be7 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -88,6 +88,8 @@ type VolumeSource struct { ISCSI *ISCSIVolumeSource `json:"iscsi" description:"iSCSI disk attached to host machine on demand"` // Glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime Glusterfs *GlusterfsVolumeSource `json:"glusterfs" description:"Glusterfs volume that will be mounted on the host machine "` + // PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace + PersistentVolumeClaimVolumeSource *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty" description:"a reference to a PersistentVolumeClaim in the same namespace"` } // Similar to VolumeSource but meant for the administrator who creates PVs. @@ -107,6 +109,14 @@ type PersistentVolumeSource struct { Glusterfs *GlusterfsVolumeSource `json:"glusterfs" description:"Glusterfs volume resource provisioned by an admin"` } +type PersistentVolumeClaimVolumeSource struct { + // ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume + ClaimName string `json:"claimName,omitempty" description:"the name of the claim in the same namespace to be mounted as a volume"` + // Optional: Defaults to false (read/write). ReadOnly here + // will force the ReadOnly setting in VolumeMounts + ReadOnly bool `json:"readOnly,omitempty" description:"mount volume as read-only when true; default false"` +} + type PersistentVolume struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index bca81abb828..1c02a1ae596 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -220,6 +220,16 @@ type VolumeSource struct { ISCSI *ISCSIVolumeSource `json:"iscsi" description:"iSCSI disk attached to host machine on demand"` // Glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime Glusterfs *GlusterfsVolumeSource `json:"glusterfs" description:"Glusterfs volume that will be mounted on the host machine "` + // PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace + PersistentVolumeClaimVolumeSource *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty" description:"a reference to a PersistentVolumeClaim in the same namespace"` +} + +type PersistentVolumeClaimVolumeSource struct { + // ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume + ClaimName string `json:"claimName,omitempty" description:"the name of the claim in the same namespace to be mounted as a volume"` + // Optional: Defaults to false (read/write). ReadOnly here + // will force the ReadOnly setting in VolumeMounts + ReadOnly bool `json:"readOnly,omitempty" description:"mount volume as read-only when true; default false"` } // Similar to VolumeSource but meant for the administrator who creates PVs. diff --git a/pkg/volume/persistent_claim/persistent_claim.go b/pkg/volume/persistent_claim/persistent_claim.go new file mode 100644 index 00000000000..3ffc4c01f20 --- /dev/null +++ b/pkg/volume/persistent_claim/persistent_claim.go @@ -0,0 +1,77 @@ +/* +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 persistent_claim + +import ( + "fmt" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + "github.com/golang/glog" +) + +func ProbeVolumePlugins() []volume.VolumePlugin { + return []volume.VolumePlugin{&persistentClaimPlugin{nil}} +} + +type persistentClaimPlugin struct { + host volume.VolumeHost +} + +var _ volume.VolumePlugin = &persistentClaimPlugin{} + +const ( + persistentClaimPluginName = "kubernetes.io/persistent-claim" +) + +func (plugin *persistentClaimPlugin) Init(host volume.VolumeHost) { + plugin.host = host +} + +func (plugin *persistentClaimPlugin) Name() string { + return persistentClaimPluginName +} + +func (plugin *persistentClaimPlugin) CanSupport(spec *volume.Spec) bool { + return spec.VolumeSource.PersistentVolumeClaimVolumeSource != nil +} + +func (plugin *persistentClaimPlugin) NewBuilder(spec *volume.Spec, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) { + claim, err := plugin.host.GetKubeClient().PersistentVolumeClaims(podRef.Namespace).Get(spec.VolumeSource.PersistentVolumeClaimVolumeSource.ClaimName) + if err != nil { + glog.Errorf("Error finding claim: %+v\n", spec.VolumeSource.PersistentVolumeClaimVolumeSource.ClaimName) + return nil, err + } + + pv, err := plugin.host.GetKubeClient().PersistentVolumes().Get(claim.Status.VolumeRef.Name) + if err != nil { + glog.Errorf("Error finding persistent volume for claim: %+v\n", claim.Name) + return nil, err + } + + builder, err := plugin.host.NewWrapperBuilder(volume.NewSpecFromPersistentVolume(pv), podRef, opts) + if err != nil { + glog.Errorf("Error creating builder for claim: %+v\n", claim.Name) + return nil, err + } + + return builder, nil +} + +func (plugin *persistentClaimPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { + return nil, fmt.Errorf("This will never be called directly. The PV backing this claim has a cleaner. Kubelet uses that cleaner, not this one, when removing orphaned volumes.") +} diff --git a/pkg/volume/persistent_claim/persistent_claim_test.go b/pkg/volume/persistent_claim/persistent_claim_test.go new file mode 100644 index 00000000000..186eb655826 --- /dev/null +++ b/pkg/volume/persistent_claim/persistent_claim_test.go @@ -0,0 +1,194 @@ +/* +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 persistent_claim + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/gce_pd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/host_path" +) + +func newTestHost(t *testing.T, fakeKubeClient client.Interface) volume.VolumeHost { + tempDir, err := ioutil.TempDir("/tmp", "persistent_volume_test.") + if err != nil { + t.Fatalf("can't make a temp rootdir: %v", err) + } + return volume.NewFakeVolumeHost(tempDir, fakeKubeClient, testProbeVolumePlugins()) +} + +func TestCanSupport(t *testing.T) { + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, ProbeVolumePlugins())) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/persistent-claim") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if plug.Name() != "kubernetes.io/persistent-claim" { + t.Errorf("Wrong name: %s", plug.Name()) + } + if !plug.CanSupport(&volume.Spec{Name: "foo", VolumeSource: api.VolumeSource{PersistentVolumeClaimVolumeSource: &api.PersistentVolumeClaimVolumeSource{}}}) { + t.Errorf("Expected true") + } + if plug.CanSupport(&volume.Spec{VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{}}}) { + t.Errorf("Expected false") + } + if plug.CanSupport(&volume.Spec{VolumeSource: api.VolumeSource{}}) { + t.Errorf("Expected false") + } +} + +func TestNewBuilder(t *testing.T) { + tests := []struct { + pv *api.PersistentVolume + claim *api.PersistentVolumeClaim + plugin volume.VolumePlugin + podVolume api.VolumeSource + testFunc func(builder volume.Builder, plugin volume.VolumePlugin) error + }{ + { + pv: &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: "pvA", + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeSource: api.PersistentVolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}, + }, + ClaimRef: &api.ObjectReference{ + Name: "claimA", + }, + }, + }, + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claimA", + Namespace: "nsA", + }, + Status: api.PersistentVolumeClaimStatus{ + Phase: api.ClaimBound, + VolumeRef: &api.ObjectReference{ + Name: "pvA", + }, + }, + }, + podVolume: api.VolumeSource{ + PersistentVolumeClaimVolumeSource: &api.PersistentVolumeClaimVolumeSource{ + ReadOnly: false, + ClaimName: "claimA", + }, + }, + plugin: gce_pd.ProbeVolumePlugins()[0], + testFunc: func(builder volume.Builder, plugin volume.VolumePlugin) error { + if !strings.Contains(builder.GetPath(), util.EscapeQualifiedNameForDisk(plugin.Name())) { + return fmt.Errorf("builder path expected to contain plugin name. Got: %s", builder.GetPath()) + } + return nil + }, + }, + { + pv: &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: "pvB", + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeSource: api.PersistentVolumeSource{ + HostPath: &api.HostPathVolumeSource{Path: "/tmp"}, + }, + ClaimRef: &api.ObjectReference{ + Name: "claimB", + }, + }, + }, + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claimB", + Namespace: "nsB", + }, + Status: api.PersistentVolumeClaimStatus{ + VolumeRef: &api.ObjectReference{ + Name: "pvB", + }, + }, + }, + podVolume: api.VolumeSource{ + PersistentVolumeClaimVolumeSource: &api.PersistentVolumeClaimVolumeSource{ + ReadOnly: false, + ClaimName: "claimB", + }, + }, + plugin: host_path.ProbeVolumePlugins()[0], + testFunc: func(builder volume.Builder, plugin volume.VolumePlugin) error { + if builder.GetPath() != "/tmp" { + return fmt.Errorf("Expected HostPath.Path /tmp, got: %s", builder.GetPath()) + } + return nil + }, + }, + } + + for _, item := range tests { + o := testclient.NewObjects(api.Scheme) + o.Add(item.pv) + o.Add(item.claim) + client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} + + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(testProbeVolumePlugins(), newTestHost(t, client)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/persistent-claim") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + spec := &volume.Spec{ + Name: "vol1", + VolumeSource: item.podVolume, + } + builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder: %v", builder) + } + + if builder != nil { + if err := item.testFunc(builder, item.plugin); err != nil { + t.Errorf("Unexpected error %+v", err) + } + } + } +} + +func testProbeVolumePlugins() []volume.VolumePlugin { + allPlugins := []volume.VolumePlugin{} + allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, host_path.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, ProbeVolumePlugins()...) + return allPlugins +}