Skip to content

Commit 0e57532

Browse files
author
Kiran Mova
authored
feat(hostpath): allow custom node affinity label (#15)
Ref: openebs/openebs#2875 provide a feature for administrators to configure a custom node affinity label in place of hostnames. This will help in scenarios, where hostnames can change when node are removed and added back to the cluster with the underlying disks intact. cluster admin can setup custom labels to the nodes and provide this information to Local PV hostpath provisioner to use via StorageClass config key called `NodeAffinityLabel` ``` + //Example: Local PV device StorageClass for using a custom + //node label as: openebs.io/node-affinity-value + //will be as follows + // + // kind: StorageClass + // metadata: + // name: openebs-hostpath + // annotations: + // openebs.io/cas-type: local + // cas.openebs.io/config: | + // - name: StorageType + // value: "device" + // - name: NodeAffinityLabel + // value: "openebs.io/node-affinity-value" + // provisioner: openebs.io/local + // volumeBindingMode: WaitForFirstConsumer + // reclaimPolicy: Delete + // ``` Signed-off-by: kmova <[email protected]>
1 parent 16ee0c1 commit 0e57532

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+8696
-36
lines changed

cmd/provisioner-localpv/app/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ const (
7878
//
7979
KeyBDTag = "BlockDeviceTag"
8080

81+
//KeyNodeAffinityLabel defines the label key that should be
82+
//used in the nodeAffinitySpec. Default is to use "kubernetes.io/hostname"
83+
//
84+
//Example: Local PV device StorageClass for using a custom
85+
//node label as: openebs.io/node-affinity-value
86+
//will be as follows
87+
//
88+
// kind: StorageClass
89+
// metadata:
90+
// name: openebs-device-tag-x
91+
// annotations:
92+
// openebs.io/cas-type: local
93+
// cas.openebs.io/config: |
94+
// - name: StorageType
95+
// value: "device"
96+
// - name: NodeAffinityLabel
97+
// value: "openebs.io/node-affinity-value"
98+
// provisioner: openebs.io/local
99+
// volumeBindingMode: WaitForFirstConsumer
100+
// reclaimPolicy: Delete
101+
//
102+
KeyNodeAffinityLabel = "NodeAffinityLabel"
103+
81104
//KeyPVRelativePath defines the alternate folder name under the BasePath
82105
// By default, the pv name will be used as the folder name.
83106
// KeyPVBasePath can be useful for providing the same underlying folder
@@ -188,6 +211,18 @@ func (c *VolumeConfig) GetBDTagValue() string {
188211
return bdTagValue
189212
}
190213

214+
//GetNodeAffinityLabelKey returns the custom node affinity
215+
//label key as configured in StorageClass.
216+
//
217+
//Default is "", use the standard kubernetes.io/hostname label.
218+
func (c *VolumeConfig) GetNodeAffinityLabelKey() string {
219+
nodeAffinityLabelKey := c.getValue(KeyNodeAffinityLabel)
220+
if len(strings.TrimSpace(nodeAffinityLabelKey)) == 0 {
221+
return ""
222+
}
223+
return nodeAffinityLabelKey
224+
}
225+
191226
//GetPath returns a valid PV path based on the configuration
192227
// or an error. The Path is constructed using the following rules:
193228
// If AbsolutePath is specified return it. (Future)
@@ -277,6 +312,16 @@ func GetNodeHostname(n *v1.Node) string {
277312
return hostname
278313
}
279314

315+
// GetNodeLabelValue extracts the value from the given label on the Node
316+
// If specificed label is not present an empty string is returned.
317+
func GetNodeLabelValue(n *v1.Node, labelKey string) string {
318+
labelValue, found := n.Labels[labelKey]
319+
if !found {
320+
return ""
321+
}
322+
return labelValue
323+
}
324+
280325
// GetTaints extracts the Taints from the Spec on the node
281326
// If Taints are empty, it just returns empty structure of corev1.Taints
282327
func GetTaints(n *v1.Node) []v1.Taint {

cmd/provisioner-localpv/app/helper_hostpath.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import (
2828

2929
hostpath "github.com/openebs/maya/pkg/hostpath/v1alpha1"
3030

31-
container "github.com/openebs/maya/pkg/kubernetes/container/v1alpha1"
32-
pod "github.com/openebs/maya/pkg/kubernetes/pod/v1alpha1"
33-
volume "github.com/openebs/maya/pkg/kubernetes/volume/v1alpha1"
31+
"github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/container"
32+
"github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/pod"
33+
"github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/volume"
3434
corev1 "k8s.io/api/core/v1"
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3636
)
@@ -52,8 +52,11 @@ var (
5252
// to execute a command (cmdsForPath) on a given
5353
// volume path (path)
5454
type HelperPodOptions struct {
55-
//nodeHostname represents the hostname of the node where pod should be launched.
56-
nodeHostname string
55+
//nodeAffinityLabelKey represents the label key of the node where pod should be launched.
56+
nodeAffinityLabelKey string
57+
58+
//nodeAffinityLabelValue represents the label value of the node where pod should be launched.
59+
nodeAffinityLabelValue string
5760

5861
//name is the name of the PV for which the pod is being launched
5962
name string
@@ -78,7 +81,8 @@ type HelperPodOptions struct {
7881
func (pOpts *HelperPodOptions) validate() error {
7982
if pOpts.name == "" ||
8083
pOpts.path == "" ||
81-
pOpts.nodeHostname == "" ||
84+
pOpts.nodeAffinityLabelKey == "" ||
85+
pOpts.nodeAffinityLabelValue == "" ||
8286
pOpts.serviceAccountName == "" {
8387
return errors.Errorf("invalid empty name or hostpath or hostname or service account name")
8488
}
@@ -165,9 +169,10 @@ func (p *Provisioner) launchPod(config podConfig) (*corev1.Pod, error) {
165169
privileged := true
166170

167171
helperPod, err := pod.NewBuilder().
168-
WithName(config.podName + "-" + config.pOpts.name).
172+
WithName(config.podName+"-"+config.pOpts.name).
169173
WithRestartPolicy(corev1.RestartPolicyNever).
170-
WithNodeSelectorHostnameNew(config.pOpts.nodeHostname).
174+
//WithNodeSelectorHostnameNew(config.pOpts.nodeHostname).
175+
WithNodeAffinityNew(config.pOpts.nodeAffinityLabelKey, config.pOpts.nodeAffinityLabelValue).
171176
WithServiceAccountName(config.pOpts.serviceAccountName).
172177
WithTolerationsForTaints(config.taints...).
173178
WithContainerBuilder(

cmd/provisioner-localpv/app/provisioner_hostpath.go

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,25 @@ import (
2626

2727
pvController "sigs.k8s.io/sig-storage-lib-external-provisioner/controller"
2828
//pvController "github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/controller"
29+
"github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/persistentvolume"
2930
mconfig "github.com/openebs/maya/pkg/apis/openebs.io/v1alpha1"
30-
persistentvolume "github.com/openebs/maya/pkg/kubernetes/persistentvolume/v1alpha1"
3131
)
3232

3333
// ProvisionHostPath is invoked by the Provisioner which expect HostPath PV
3434
// to be provisioned and a valid PV spec returned.
3535
func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volumeConfig *VolumeConfig) (*v1.PersistentVolume, error) {
3636
pvc := opts.PVC
37-
nodeHostname := GetNodeHostname(opts.SelectedNode)
3837
taints := GetTaints(opts.SelectedNode)
3938
name := opts.PVName
4039
stgType := volumeConfig.GetStorageType()
4140
saName := getOpenEBSServiceAccountName()
4241

42+
nodeAffinityKey := volumeConfig.GetNodeAffinityLabelKey()
43+
if len(nodeAffinityKey) == 0 {
44+
nodeAffinityKey = k8sNodeLabelKeyHostname
45+
}
46+
nodeAffinityValue := GetNodeLabelValue(opts.SelectedNode, nodeAffinityKey)
47+
4348
path, err := volumeConfig.GetPath()
4449
if err != nil {
4550
alertlog.Logger.Errorw("",
@@ -52,17 +57,18 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu
5257
return nil, err
5358
}
5459

55-
klog.Infof("Creating volume %v at %v:%v", name, nodeHostname, path)
60+
klog.Infof("Creating volume %v at node with label %v=%v, path:%v", name, nodeAffinityKey, nodeAffinityValue, path)
5661

5762
//Before using the path for local PV, make sure it is created.
5863
initCmdsForPath := []string{"mkdir", "-m", "0777", "-p"}
5964
podOpts := &HelperPodOptions{
60-
cmdsForPath: initCmdsForPath,
61-
name: name,
62-
path: path,
63-
nodeHostname: nodeHostname,
64-
serviceAccountName: saName,
65-
selectedNodeTaints: taints,
65+
cmdsForPath: initCmdsForPath,
66+
name: name,
67+
path: path,
68+
nodeAffinityLabelKey: nodeAffinityKey,
69+
nodeAffinityLabelValue: nodeAffinityValue,
70+
serviceAccountName: saName,
71+
selectedNodeTaints: taints,
6672
}
6773
iErr := p.createInitPod(podOpts)
6874
if iErr != nil {
@@ -104,7 +110,7 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu
104110
WithVolumeMode(fs).
105111
WithCapacityQty(pvc.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]).
106112
WithLocalHostDirectory(path).
107-
WithNodeAffinity(nodeHostname).
113+
WithNodeAffinity(nodeAffinityKey, nodeAffinityValue).
108114
Build()
109115

110116
if err != nil {
@@ -126,20 +132,25 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu
126132
return pvObj, nil
127133
}
128134

129-
// GetNodeObjectFromHostName returns the Node Object with matching NodeHostName.
130-
func (p *Provisioner) GetNodeObjectFromHostName(hostName string) (*v1.Node, error) {
131-
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{persistentvolume.KeyNode: hostName}}
135+
// GetNodeObjectFromLabels returns the Node Object with matching label key and value
136+
func (p *Provisioner) GetNodeObjectFromLabels(key, value string) (*v1.Node, error) {
137+
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{key: value}}
132138
listOptions := metav1.ListOptions{
133139
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
134-
Limit: 1,
135140
}
136141
nodeList, err := p.kubeClient.CoreV1().Nodes().List(listOptions)
137142
if err != nil || len(nodeList.Items) == 0 {
138143
// After the PV is created and node affinity is set
139144
// based on kubernetes.io/hostname label, either:
140145
// - hostname label changed on the node or
141146
// - the node is deleted from the cluster.
142-
return nil, errors.Errorf("Unable to get the Node with the NodeHostName [%s]", hostName)
147+
return nil, errors.Errorf("Unable to get the Node with the Node Label %s [%s]", key, value)
148+
}
149+
if len(nodeList.Items) != 1 {
150+
// After the PV is created and node affinity is set
151+
// on a custom affinity label, there may be a transitory state
152+
// with two nodes matching (old and new) label.
153+
return nil, errors.Errorf("Unable to determine the Node. Found multiple nodes matching the labels %s [%s].", key, value)
143154
}
144155
return &nodeList.Items[0], nil
145156

@@ -162,28 +173,29 @@ func (p *Provisioner) DeleteHostPath(pv *v1.PersistentVolume) (err error) {
162173
return errors.Errorf("no HostPath set")
163174
}
164175

165-
hostname := pvObj.GetAffinitedNodeHostname()
166-
if hostname == "" {
167-
return errors.Errorf("cannot find affinited node hostname")
176+
nodeAffinityKey, nodeAffinityValue := pvObj.GetAffinitedNodeLabelKeyAndValue()
177+
if nodeAffinityValue == "" {
178+
return errors.Errorf("cannot find affinited node details")
168179
}
169-
alertlog.Logger.Infof("Get the Node Object from hostName: %v", hostname)
180+
alertlog.Logger.Infof("Get the Node Object with label %v : %v", nodeAffinityKey, nodeAffinityValue)
170181

171182
//Get the node Object once again to get updated Taints.
172-
nodeObject, err := p.GetNodeObjectFromHostName(hostname)
183+
nodeObject, err := p.GetNodeObjectFromLabels(nodeAffinityKey, nodeAffinityValue)
173184
if err != nil {
174185
return err
175186
}
176187
taints := GetTaints(nodeObject)
177188
//Initiate clean up only when reclaim policy is not retain.
178-
klog.Infof("Deleting volume %v at %v:%v", pv.Name, hostname, path)
189+
klog.Infof("Deleting volume %v at %v:%v", pv.Name, GetNodeHostname(nodeObject), path)
179190
cleanupCmdsForPath := []string{"rm", "-rf"}
180191
podOpts := &HelperPodOptions{
181-
cmdsForPath: cleanupCmdsForPath,
182-
name: pv.Name,
183-
path: path,
184-
nodeHostname: hostname,
185-
serviceAccountName: saName,
186-
selectedNodeTaints: taints,
192+
cmdsForPath: cleanupCmdsForPath,
193+
name: pv.Name,
194+
path: path,
195+
nodeAffinityLabelKey: nodeAffinityKey,
196+
nodeAffinityLabelValue: nodeAffinityValue,
197+
serviceAccountName: saName,
198+
selectedNodeTaints: taints,
187199
}
188200

189201
if err := p.createCleanupPod(podOpts); err != nil {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: busybox
5+
namespace: default
6+
spec:
7+
containers:
8+
- command:
9+
- sh
10+
- -c
11+
- 'date >> /mnt/store1/date.txt; hostname >> /mnt/store1/hostname.txt; sync; sleep 5; sync; tail -f /dev/null;'
12+
image: busybox
13+
imagePullPolicy: Always
14+
name: busybox
15+
volumeMounts:
16+
- mountPath: /mnt/store1
17+
name: demo-vol1
18+
volumes:
19+
- name: demo-vol1
20+
persistentVolumeClaim:
21+
claimName: demo-vol1-claim
22+
---
23+
kind: PersistentVolumeClaim
24+
apiVersion: v1
25+
metadata:
26+
name: demo-vol1-claim
27+
spec:
28+
storageClassName: openebs-hostpath
29+
accessModes:
30+
- ReadWriteOnce
31+
resources:
32+
requests:
33+
storage: 5G
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: fillup
5+
namespace: default
6+
spec:
7+
containers:
8+
- command:
9+
- sh
10+
- -c
11+
- 'dd if=/dev/zero of=/mnt/store1/dump.dd bs=1M; sync; sleep 5; sync; tail -f /dev/null;'
12+
image: busybox
13+
imagePullPolicy: Always
14+
name: fillup-bb
15+
volumeMounts:
16+
- mountPath: /mnt/store1
17+
name: fillup
18+
volumes:
19+
- name: fillup
20+
persistentVolumeClaim:
21+
claimName: fillup-claim
22+
---
23+
kind: PersistentVolumeClaim
24+
apiVersion: v1
25+
metadata:
26+
name: fillup-claim
27+
spec:
28+
storageClassName: openebs-hostpath
29+
accessModes:
30+
- ReadWriteOnce
31+
resources:
32+
requests:
33+
storage: 1G
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#Sample storage classes for OpenEBS Local PV
2+
apiVersion: storage.k8s.io/v1
3+
kind: StorageClass
4+
metadata:
5+
name: openebs-hostpath
6+
annotations:
7+
openebs.io/cas-type: local
8+
cas.openebs.io/config: |
9+
#hostpath type will create a PV by
10+
# creating a sub-directory under the
11+
# BASEPATH provided below.
12+
- name: StorageType
13+
value: "hostpath"
14+
#Specify the location (directory) where
15+
# where PV(volume) data will be saved.
16+
# A sub-directory with pv-name will be
17+
# created. When the volume is deleted,
18+
# the PV sub-directory will be deleted.
19+
#Default value is /var/openebs/local
20+
- name: BasePath
21+
value: "/var/openebs/local/"
22+
#Specify the node affinity label
23+
# to be added to the PV
24+
#Default: kubernetes.io/hostname
25+
#- name: NodeAffinityLabel
26+
# value: "openebs.io/stg-node-name"
27+
provisioner: openebs.io/local
28+
volumeBindingMode: WaitForFirstConsumer
29+
reclaimPolicy: Delete

0 commit comments

Comments
 (0)