gce_pd.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package gcepd
  14. import (
  15. "context"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "runtime"
  20. "strconv"
  21. "strings"
  22. "k8s.io/api/core/v1"
  23. "k8s.io/apimachinery/pkg/api/resource"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/types"
  26. utilfeature "k8s.io/apiserver/pkg/util/feature"
  27. volumehelpers "k8s.io/cloud-provider/volume/helpers"
  28. "k8s.io/klog"
  29. "k8s.io/kubernetes/pkg/features"
  30. "k8s.io/kubernetes/pkg/util/mount"
  31. "k8s.io/kubernetes/pkg/volume"
  32. "k8s.io/kubernetes/pkg/volume/util"
  33. gcecloud "k8s.io/legacy-cloud-providers/gce"
  34. utilstrings "k8s.io/utils/strings"
  35. )
  36. // ProbeVolumePlugins is the primary entrypoint for volume plugins.
  37. func ProbeVolumePlugins() []volume.VolumePlugin {
  38. return []volume.VolumePlugin{&gcePersistentDiskPlugin{nil}}
  39. }
  40. type gcePersistentDiskPlugin struct {
  41. host volume.VolumeHost
  42. }
  43. var _ volume.VolumePlugin = &gcePersistentDiskPlugin{}
  44. var _ volume.PersistentVolumePlugin = &gcePersistentDiskPlugin{}
  45. var _ volume.DeletableVolumePlugin = &gcePersistentDiskPlugin{}
  46. var _ volume.ProvisionableVolumePlugin = &gcePersistentDiskPlugin{}
  47. var _ volume.ExpandableVolumePlugin = &gcePersistentDiskPlugin{}
  48. var _ volume.VolumePluginWithAttachLimits = &gcePersistentDiskPlugin{}
  49. const (
  50. gcePersistentDiskPluginName = "kubernetes.io/gce-pd"
  51. )
  52. // The constants are used to map from the machine type (number of CPUs) to the limit of
  53. // persistent disks that can be attached to an instance. Please refer to gcloud doc
  54. // https://cloud.google.com/compute/docs/disks/#increased_persistent_disk_limits
  55. // These constants are all the documented attach limit minus one because the
  56. // node boot disk is considered an attachable disk so effective attach limit is
  57. // one less.
  58. const (
  59. volumeLimitSmall = 15
  60. VolumeLimitBig = 127
  61. )
  62. func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
  63. return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(gcePersistentDiskPluginName), volName)
  64. }
  65. func (plugin *gcePersistentDiskPlugin) Init(host volume.VolumeHost) error {
  66. plugin.host = host
  67. return nil
  68. }
  69. func (plugin *gcePersistentDiskPlugin) GetPluginName() string {
  70. return gcePersistentDiskPluginName
  71. }
  72. func (plugin *gcePersistentDiskPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
  73. volumeSource, _, err := getVolumeSource(spec)
  74. if err != nil {
  75. return "", err
  76. }
  77. return volumeSource.PDName, nil
  78. }
  79. func (plugin *gcePersistentDiskPlugin) CanSupport(spec *volume.Spec) bool {
  80. return (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.GCEPersistentDisk != nil) ||
  81. (spec.Volume != nil && spec.Volume.GCEPersistentDisk != nil)
  82. }
  83. func (plugin *gcePersistentDiskPlugin) IsMigratedToCSI() bool {
  84. return utilfeature.DefaultFeatureGate.Enabled(features.CSIMigration) &&
  85. utilfeature.DefaultFeatureGate.Enabled(features.CSIMigrationGCE)
  86. }
  87. func (plugin *gcePersistentDiskPlugin) RequiresRemount() bool {
  88. return false
  89. }
  90. func (plugin *gcePersistentDiskPlugin) SupportsMountOption() bool {
  91. return true
  92. }
  93. func (plugin *gcePersistentDiskPlugin) SupportsBulkVolumeVerification() bool {
  94. return true
  95. }
  96. func (plugin *gcePersistentDiskPlugin) GetAccessModes() []v1.PersistentVolumeAccessMode {
  97. return []v1.PersistentVolumeAccessMode{
  98. v1.ReadWriteOnce,
  99. v1.ReadOnlyMany,
  100. }
  101. }
  102. func (plugin *gcePersistentDiskPlugin) GetVolumeLimits() (map[string]int64, error) {
  103. volumeLimits := map[string]int64{
  104. util.GCEVolumeLimitKey: volumeLimitSmall,
  105. }
  106. cloud := plugin.host.GetCloudProvider()
  107. // if we can't fetch cloudprovider we return an error
  108. // hoping external CCM or admin can set it. Returning
  109. // default values from here will mean, no one can
  110. // override them.
  111. if cloud == nil {
  112. return nil, fmt.Errorf("No cloudprovider present")
  113. }
  114. if cloud.ProviderName() != gcecloud.ProviderName {
  115. return nil, fmt.Errorf("Expected gce cloud got %s", cloud.ProviderName())
  116. }
  117. instances, ok := cloud.Instances()
  118. if !ok {
  119. klog.Warning("Failed to get instances from cloud provider")
  120. return volumeLimits, nil
  121. }
  122. instanceType, err := instances.InstanceType(context.TODO(), plugin.host.GetNodeName())
  123. if err != nil {
  124. klog.Errorf("Failed to get instance type from GCE cloud provider")
  125. return volumeLimits, nil
  126. }
  127. if strings.HasPrefix(instanceType, "n1-") || strings.HasPrefix(instanceType, "custom-") {
  128. volumeLimits[util.GCEVolumeLimitKey] = VolumeLimitBig
  129. } else {
  130. volumeLimits[util.GCEVolumeLimitKey] = volumeLimitSmall
  131. }
  132. return volumeLimits, nil
  133. }
  134. func (plugin *gcePersistentDiskPlugin) VolumeLimitKey(spec *volume.Spec) string {
  135. return util.GCEVolumeLimitKey
  136. }
  137. func (plugin *gcePersistentDiskPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) {
  138. // Inject real implementations here, test through the internal function.
  139. return plugin.newMounterInternal(spec, pod.UID, &GCEDiskUtil{}, plugin.host.GetMounter(plugin.GetPluginName()))
  140. }
  141. func getVolumeSource(
  142. spec *volume.Spec) (*v1.GCEPersistentDiskVolumeSource, bool, error) {
  143. if spec.Volume != nil && spec.Volume.GCEPersistentDisk != nil {
  144. return spec.Volume.GCEPersistentDisk, spec.Volume.GCEPersistentDisk.ReadOnly, nil
  145. } else if spec.PersistentVolume != nil &&
  146. spec.PersistentVolume.Spec.GCEPersistentDisk != nil {
  147. return spec.PersistentVolume.Spec.GCEPersistentDisk, spec.ReadOnly, nil
  148. }
  149. return nil, false, fmt.Errorf("Spec does not reference a GCE volume type")
  150. }
  151. func (plugin *gcePersistentDiskPlugin) newMounterInternal(spec *volume.Spec, podUID types.UID, manager pdManager, mounter mount.Interface) (volume.Mounter, error) {
  152. // GCEPDs used directly in a pod have a ReadOnly flag set by the pod author.
  153. // GCEPDs used as a PersistentVolume gets the ReadOnly flag indirectly through the persistent-claim volume used to mount the PV
  154. volumeSource, readOnly, err := getVolumeSource(spec)
  155. if err != nil {
  156. return nil, err
  157. }
  158. pdName := volumeSource.PDName
  159. partition := ""
  160. if volumeSource.Partition != 0 {
  161. partition = strconv.Itoa(int(volumeSource.Partition))
  162. }
  163. return &gcePersistentDiskMounter{
  164. gcePersistentDisk: &gcePersistentDisk{
  165. podUID: podUID,
  166. volName: spec.Name(),
  167. pdName: pdName,
  168. partition: partition,
  169. mounter: mounter,
  170. manager: manager,
  171. plugin: plugin,
  172. MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, spec.Name(), plugin.host)),
  173. },
  174. mountOptions: util.MountOptionFromSpec(spec),
  175. readOnly: readOnly}, nil
  176. }
  177. func (plugin *gcePersistentDiskPlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) {
  178. // Inject real implementations here, test through the internal function.
  179. return plugin.newUnmounterInternal(volName, podUID, &GCEDiskUtil{}, plugin.host.GetMounter(plugin.GetPluginName()))
  180. }
  181. func (plugin *gcePersistentDiskPlugin) newUnmounterInternal(volName string, podUID types.UID, manager pdManager, mounter mount.Interface) (volume.Unmounter, error) {
  182. return &gcePersistentDiskUnmounter{&gcePersistentDisk{
  183. podUID: podUID,
  184. volName: volName,
  185. manager: manager,
  186. mounter: mounter,
  187. plugin: plugin,
  188. MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, volName, plugin.host)),
  189. }}, nil
  190. }
  191. func (plugin *gcePersistentDiskPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) {
  192. return plugin.newDeleterInternal(spec, &GCEDiskUtil{})
  193. }
  194. func (plugin *gcePersistentDiskPlugin) newDeleterInternal(spec *volume.Spec, manager pdManager) (volume.Deleter, error) {
  195. if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.GCEPersistentDisk == nil {
  196. return nil, fmt.Errorf("spec.PersistentVolumeSource.GCEPersistentDisk is nil")
  197. }
  198. return &gcePersistentDiskDeleter{
  199. gcePersistentDisk: &gcePersistentDisk{
  200. volName: spec.Name(),
  201. pdName: spec.PersistentVolume.Spec.GCEPersistentDisk.PDName,
  202. manager: manager,
  203. plugin: plugin,
  204. }}, nil
  205. }
  206. func (plugin *gcePersistentDiskPlugin) NewProvisioner(options volume.VolumeOptions) (volume.Provisioner, error) {
  207. return plugin.newProvisionerInternal(options, &GCEDiskUtil{})
  208. }
  209. func (plugin *gcePersistentDiskPlugin) newProvisionerInternal(options volume.VolumeOptions, manager pdManager) (volume.Provisioner, error) {
  210. return &gcePersistentDiskProvisioner{
  211. gcePersistentDisk: &gcePersistentDisk{
  212. manager: manager,
  213. plugin: plugin,
  214. },
  215. options: options,
  216. }, nil
  217. }
  218. func (plugin *gcePersistentDiskPlugin) RequiresFSResize() bool {
  219. return true
  220. }
  221. func (plugin *gcePersistentDiskPlugin) ExpandVolumeDevice(
  222. spec *volume.Spec,
  223. newSize resource.Quantity,
  224. oldSize resource.Quantity) (resource.Quantity, error) {
  225. cloud, err := getCloudProvider(plugin.host.GetCloudProvider())
  226. if err != nil {
  227. return oldSize, err
  228. }
  229. pdName := spec.PersistentVolume.Spec.GCEPersistentDisk.PDName
  230. updatedQuantity, err := cloud.ResizeDisk(pdName, oldSize, newSize)
  231. if err != nil {
  232. return oldSize, err
  233. }
  234. return updatedQuantity, nil
  235. }
  236. func (plugin *gcePersistentDiskPlugin) NodeExpand(resizeOptions volume.NodeResizeOptions) (bool, error) {
  237. _, err := util.GenericResizeFS(plugin.host, plugin.GetPluginName(), resizeOptions.DevicePath, resizeOptions.DeviceMountPath)
  238. if err != nil {
  239. return false, err
  240. }
  241. return true, nil
  242. }
  243. var _ volume.NodeExpandableVolumePlugin = &gcePersistentDiskPlugin{}
  244. func (plugin *gcePersistentDiskPlugin) ConstructVolumeSpec(volumeName, mountPath string) (*volume.Spec, error) {
  245. mounter := plugin.host.GetMounter(plugin.GetPluginName())
  246. pluginMntDir := util.GetPluginMountDir(plugin.host, plugin.GetPluginName())
  247. sourceName, err := mounter.GetDeviceNameFromMount(mountPath, pluginMntDir)
  248. if err != nil {
  249. return nil, err
  250. }
  251. gceVolume := &v1.Volume{
  252. Name: volumeName,
  253. VolumeSource: v1.VolumeSource{
  254. GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{
  255. PDName: sourceName,
  256. },
  257. },
  258. }
  259. return volume.NewSpecFromVolume(gceVolume), nil
  260. }
  261. // Abstract interface to PD operations.
  262. type pdManager interface {
  263. // Creates a volume
  264. CreateVolume(provisioner *gcePersistentDiskProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error)
  265. // Deletes a volume
  266. DeleteVolume(deleter *gcePersistentDiskDeleter) error
  267. }
  268. // gcePersistentDisk volumes are disk resources provided by Google Compute Engine
  269. // that are attached to the kubelet's host machine and exposed to the pod.
  270. type gcePersistentDisk struct {
  271. volName string
  272. podUID types.UID
  273. // Unique identifier of the PD, used to find the disk resource in the provider.
  274. pdName string
  275. // Specifies the partition to mount
  276. partition string
  277. // Utility interface to provision and delete disks
  278. manager pdManager
  279. // Mounter interface that provides system calls to mount the global path to the pod local path.
  280. mounter mount.Interface
  281. plugin *gcePersistentDiskPlugin
  282. volume.MetricsProvider
  283. }
  284. type gcePersistentDiskMounter struct {
  285. *gcePersistentDisk
  286. // Specifies whether the disk will be mounted as read-only.
  287. readOnly bool
  288. mountOptions []string
  289. }
  290. var _ volume.Mounter = &gcePersistentDiskMounter{}
  291. func (b *gcePersistentDiskMounter) GetAttributes() volume.Attributes {
  292. return volume.Attributes{
  293. ReadOnly: b.readOnly,
  294. Managed: !b.readOnly,
  295. SupportsSELinux: true,
  296. }
  297. }
  298. // Checks prior to mount operations to verify that the required components (binaries, etc.)
  299. // to mount the volume are available on the underlying node.
  300. // If not, it returns an error
  301. func (b *gcePersistentDiskMounter) CanMount() error {
  302. return nil
  303. }
  304. // SetUp bind mounts the disk global mount to the volume path.
  305. func (b *gcePersistentDiskMounter) SetUp(mounterArgs volume.MounterArgs) error {
  306. return b.SetUpAt(b.GetPath(), mounterArgs)
  307. }
  308. // SetUp bind mounts the disk global mount to the give volume path.
  309. func (b *gcePersistentDiskMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
  310. // TODO: handle failed mounts here.
  311. notMnt, err := b.mounter.IsLikelyNotMountPoint(dir)
  312. klog.V(4).Infof("GCE PersistentDisk set up: Dir (%s) PD name (%q) Mounted (%t) Error (%v), ReadOnly (%t)", dir, b.pdName, !notMnt, err, b.readOnly)
  313. if err != nil && !os.IsNotExist(err) {
  314. klog.Errorf("cannot validate mount point: %s %v", dir, err)
  315. return err
  316. }
  317. if !notMnt {
  318. return nil
  319. }
  320. if runtime.GOOS != "windows" {
  321. // in windows, we will use mklink to mount, will MkdirAll in Mount func
  322. if err := os.MkdirAll(dir, 0750); err != nil {
  323. klog.Errorf("mkdir failed on disk %s (%v)", dir, err)
  324. return err
  325. }
  326. }
  327. // Perform a bind mount to the full path to allow duplicate mounts of the same PD.
  328. options := []string{"bind"}
  329. if b.readOnly {
  330. options = append(options, "ro")
  331. }
  332. globalPDPath := makeGlobalPDName(b.plugin.host, b.pdName)
  333. klog.V(4).Infof("attempting to mount %s", dir)
  334. mountOptions := util.JoinMountOptions(b.mountOptions, options)
  335. err = b.mounter.Mount(globalPDPath, dir, "", mountOptions)
  336. if err != nil {
  337. notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
  338. if mntErr != nil {
  339. klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
  340. return err
  341. }
  342. if !notMnt {
  343. if mntErr = b.mounter.Unmount(dir); mntErr != nil {
  344. klog.Errorf("Failed to unmount: %v", mntErr)
  345. return err
  346. }
  347. notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
  348. if mntErr != nil {
  349. klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
  350. return err
  351. }
  352. if !notMnt {
  353. // This is very odd, we don't expect it. We'll try again next sync loop.
  354. klog.Errorf("%s is still mounted, despite call to unmount(). Will try again next sync loop.", dir)
  355. return err
  356. }
  357. }
  358. os.Remove(dir)
  359. klog.Errorf("Mount of disk %s failed: %v", dir, err)
  360. return err
  361. }
  362. if !b.readOnly {
  363. volume.SetVolumeOwnership(b, mounterArgs.FsGroup)
  364. }
  365. klog.V(4).Infof("Successfully mounted %s", dir)
  366. return nil
  367. }
  368. func makeGlobalPDName(host volume.VolumeHost, devName string) string {
  369. return filepath.Join(host.GetPluginDir(gcePersistentDiskPluginName), util.MountsInGlobalPDPath, devName)
  370. }
  371. func (b *gcePersistentDiskMounter) GetPath() string {
  372. return getPath(b.podUID, b.volName, b.plugin.host)
  373. }
  374. type gcePersistentDiskUnmounter struct {
  375. *gcePersistentDisk
  376. }
  377. var _ volume.Unmounter = &gcePersistentDiskUnmounter{}
  378. func (c *gcePersistentDiskUnmounter) GetPath() string {
  379. return getPath(c.podUID, c.volName, c.plugin.host)
  380. }
  381. // Unmounts the bind mount, and detaches the disk only if the PD
  382. // resource was the last reference to that disk on the kubelet.
  383. func (c *gcePersistentDiskUnmounter) TearDown() error {
  384. return c.TearDownAt(c.GetPath())
  385. }
  386. // TearDownAt unmounts the bind mount
  387. func (c *gcePersistentDiskUnmounter) TearDownAt(dir string) error {
  388. return mount.CleanupMountPoint(dir, c.mounter, false)
  389. }
  390. type gcePersistentDiskDeleter struct {
  391. *gcePersistentDisk
  392. }
  393. var _ volume.Deleter = &gcePersistentDiskDeleter{}
  394. func (d *gcePersistentDiskDeleter) GetPath() string {
  395. return getPath(d.podUID, d.volName, d.plugin.host)
  396. }
  397. func (d *gcePersistentDiskDeleter) Delete() error {
  398. return d.manager.DeleteVolume(d)
  399. }
  400. type gcePersistentDiskProvisioner struct {
  401. *gcePersistentDisk
  402. options volume.VolumeOptions
  403. }
  404. var _ volume.Provisioner = &gcePersistentDiskProvisioner{}
  405. func (c *gcePersistentDiskProvisioner) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) {
  406. if !util.AccessModesContainedInAll(c.plugin.GetAccessModes(), c.options.PVC.Spec.AccessModes) {
  407. return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", c.options.PVC.Spec.AccessModes, c.plugin.GetAccessModes())
  408. }
  409. volumeID, sizeGB, labels, fstype, err := c.manager.CreateVolume(c, selectedNode, allowedTopologies)
  410. if err != nil {
  411. return nil, err
  412. }
  413. if fstype == "" {
  414. fstype = "ext4"
  415. }
  416. var volumeMode *v1.PersistentVolumeMode
  417. if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) {
  418. volumeMode = c.options.PVC.Spec.VolumeMode
  419. if volumeMode != nil && *volumeMode == v1.PersistentVolumeBlock {
  420. // Block volumes should not have any FSType
  421. fstype = ""
  422. }
  423. }
  424. pv := &v1.PersistentVolume{
  425. ObjectMeta: metav1.ObjectMeta{
  426. Name: c.options.PVName,
  427. Labels: map[string]string{},
  428. Annotations: map[string]string{
  429. util.VolumeDynamicallyCreatedByKey: "gce-pd-dynamic-provisioner",
  430. },
  431. },
  432. Spec: v1.PersistentVolumeSpec{
  433. PersistentVolumeReclaimPolicy: c.options.PersistentVolumeReclaimPolicy,
  434. AccessModes: c.options.PVC.Spec.AccessModes,
  435. Capacity: v1.ResourceList{
  436. v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGB)),
  437. },
  438. VolumeMode: volumeMode,
  439. PersistentVolumeSource: v1.PersistentVolumeSource{
  440. GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{
  441. PDName: volumeID,
  442. Partition: 0,
  443. ReadOnly: false,
  444. FSType: fstype,
  445. },
  446. },
  447. MountOptions: c.options.MountOptions,
  448. },
  449. }
  450. if len(c.options.PVC.Spec.AccessModes) == 0 {
  451. pv.Spec.AccessModes = c.plugin.GetAccessModes()
  452. }
  453. requirements := make([]v1.NodeSelectorRequirement, 0)
  454. if len(labels) != 0 {
  455. if pv.Labels == nil {
  456. pv.Labels = make(map[string]string)
  457. }
  458. for k, v := range labels {
  459. pv.Labels[k] = v
  460. var values []string
  461. if k == v1.LabelZoneFailureDomain {
  462. values, err = volumehelpers.LabelZonesToList(v)
  463. if err != nil {
  464. return nil, fmt.Errorf("failed to convert label string for Zone: %s to a List: %v", v, err)
  465. }
  466. } else {
  467. values = []string{v}
  468. }
  469. requirements = append(requirements, v1.NodeSelectorRequirement{Key: k, Operator: v1.NodeSelectorOpIn, Values: values})
  470. }
  471. }
  472. if len(requirements) > 0 {
  473. pv.Spec.NodeAffinity = new(v1.VolumeNodeAffinity)
  474. pv.Spec.NodeAffinity.Required = new(v1.NodeSelector)
  475. pv.Spec.NodeAffinity.Required.NodeSelectorTerms = make([]v1.NodeSelectorTerm, 1)
  476. pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = requirements
  477. }
  478. return pv, nil
  479. }