Skip to content

Commit 5884d5b

Browse files
authored
Merge pull request #120 from jp39/hostpath-auto
Add "auto" provisioning type.
2 parents 829d4a5 + a234118 commit 5884d5b

File tree

7 files changed

+136
-64
lines changed

7 files changed

+136
-64
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ but the `PersistentVolume` objects will have a [NodeAffinity][node affinity] con
1515

1616
![architecture with Hostpath](architecture.hostpath.drawio.svg "Architecture with Hostpath provisioning")
1717

18+
As a third option, if the ZFS host is part of the cluster, you can let the provisioner choose
19+
whether [NFS][nfs] or [HostPath][hostpath] is used with the `Auto` mode. If the requested
20+
[AccessModes][access modes] in the Persistent Volume Claim contains `ReadWriteOnce` (the volume
21+
can only be accessed by pods running on the same node), or `ReadWriteOncePod` (the volume can only
22+
be accessed by one single Pod at any time), then [HostPath][hostpath] will be used and
23+
the [NodeAffinity][node affinity] will be configured on the `PersistentVolume` objects so the
24+
scheduler will automatically place the corresponding Pods onto the ZFS host. Otherwise
25+
[NFS][nfs] will be used and [NodeAffinity][node affinity] will not be set. If multiple (exclusive)
26+
[AccessModes][access modes] are given, [NFS][nfs] takes precedence.
27+
1828
Currently all ZFS attributes are inherited from the parent dataset.
1929

2030
For more information about external storage in kubernetes, see
@@ -85,6 +95,26 @@ parameters:
8595
```
8696
For NFS, you can also specify other options, as described in [exports(5)][man exports].
8797
98+
The following example configures a storage class using the `Auto` type. The provisioner
99+
will decide whether [HostPath][hostpath] or [NFS][nfs] will be used based on the
100+
[AccessModess][access modes] requested by the persistent volume claim.
101+
102+
```yaml
103+
kind: StorageClass
104+
apiVersion: storage.k8s.io/v1
105+
metadata:
106+
name: zfs-nfs
107+
provisioner: pv.kubernetes.io/zfs
108+
reclaimPolicy: Retain
109+
parameters:
110+
parentDataset: tank/kubernetes
111+
hostname: storage-1.domain.tld
112+
type: auto
113+
node: storage-1 # the name of the node where the ZFS datasets are located.
114+
shareProperties: rw,no_root_squash
115+
reserveSpace: true
116+
```
117+
88118
## Notes
89119

90120
### Reclaim policy
@@ -189,3 +219,4 @@ I (@ccremer) have been allowed to take over maintenance for this repository.
189219
[helm chart]: https://github.com/ccremer/kubernetes-zfs-provisioner/blob/master/charts/kubernetes-zfs-provisioner/README.md
190220
[gentics]: https://www.gentics.com/genticscms/index.en.html
191221
[gentics repo]: https://github.com/gentics/kubernetes-zfs-provisioner
222+
[access modes]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes

charts/kubernetes-zfs-provisioner/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ storageClass:
3737
# policy: "Delete"
3838
# # -- NFS export properties (see `exports(5)`)
3939
# shareProperties: ""
40-
# # -- Provision type, one of [`nfs`, `hostpath`]
40+
# # -- Provision type, one of [`nfs`, `hostpath`, `auto`]
4141
# type: "nfs"
4242
# # -- Override `kubernetes.io/hostname` from `hostName` parameter for
4343
# # `HostPath` node affinity

pkg/provisioner/parameters.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,34 @@ const (
1919
parameters:
2020
parentDataset: tank/volumes
2121
hostname: my-zfs-host.localdomain
22-
type: nfs|hostpath
22+
type: nfs|hostPath|auto
2323
shareProperties: rw=10.0.0.0/8,no_root_squash
2424
node: my-zfs-host
2525
reserveSpace: true|false
2626
*/
2727

28+
type ProvisioningType string
29+
30+
const (
31+
Nfs ProvisioningType = "nfs"
32+
HostPath ProvisioningType = "hostPath"
33+
Auto ProvisioningType = "auto"
34+
)
35+
2836
type (
2937
// ZFSStorageClassParameters represents the parameters on the `StorageClass`
3038
// object. It is used to ease access and validate those parameters at run time.
3139
ZFSStorageClassParameters struct {
3240
// ParentDataset of the zpool. Needs to be existing on the target ZFS host.
3341
ParentDataset string
3442
// Hostname of the target ZFS host. Will be used to connect over SSH.
35-
Hostname string
36-
NFS *NFSParameters
37-
HostPath *HostPathParameters
38-
ReserveSpace bool
39-
}
40-
NFSParameters struct {
41-
// ShareProperties specifies additional properties to pass to 'zfs create sharenfs=%s'.
42-
ShareProperties string
43-
}
44-
HostPathParameters struct {
45-
// NodeName overrides the hostname if the Kubernetes node name is different than the ZFS target host. Used for Affinity
46-
NodeName string
43+
Hostname string
44+
Type ProvisioningType
45+
// NFSShareProperties specifies additional properties to pass to 'zfs create sharenfs=%s'.
46+
NFSShareProperties string
47+
// HostPathNodeName overrides the hostname if the Kubernetes node name is different than the ZFS target host. Used for Affinity
48+
HostPathNodeName string
49+
ReserveSpace bool
4750
}
4851
)
4952

@@ -79,16 +82,26 @@ func NewStorageClassParameters(parameters map[string]string) (*ZFSStorageClassPa
7982
typeParam := parameters[TypeParameter]
8083
switch typeParam {
8184
case "hostpath", "hostPath", "HostPath", "Hostpath", "HOSTPATH":
82-
p.HostPath = &HostPathParameters{NodeName: parameters[NodeNameParameter]}
83-
return p, nil
85+
p.Type = HostPath
8486
case "nfs", "Nfs", "NFS":
87+
p.Type = Nfs
88+
case "auto", "Auto", "AUTO":
89+
p.Type = Auto
90+
default:
91+
return nil, fmt.Errorf("invalid '%s' parameter value: %s", TypeParameter, typeParam)
92+
}
93+
94+
if p.Type == HostPath || p.Type == Auto {
95+
p.HostPathNodeName = parameters[NodeNameParameter]
96+
}
97+
98+
if p.Type == Nfs || p.Type == Auto {
8599
shareProps := parameters[SharePropertiesParameter]
86100
if shareProps == "" {
87101
shareProps = "on"
88102
}
89-
p.NFS = &NFSParameters{ShareProperties: shareProps}
90-
return p, nil
91-
default:
92-
return nil, fmt.Errorf("invalid '%s' parameter value: %s", TypeParameter, typeParam)
103+
p.NFSShareProperties = shareProps
93104
}
105+
106+
return p, nil
94107
}

pkg/provisioner/parameters_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func TestNewStorageClassParameters(t *testing.T) {
7777
SharePropertiesParameter: "rw",
7878
},
7979
},
80-
want: &ZFSStorageClassParameters{NFS: &NFSParameters{ShareProperties: "rw"}},
80+
want: &ZFSStorageClassParameters{NFSShareProperties: "rw"},
8181
},
8282
{
8383
name: "GivenCorrectSpec_WhenTypeNfsWithoutProperties_ThenReturnNfsParametersWithDefault",
@@ -88,7 +88,7 @@ func TestNewStorageClassParameters(t *testing.T) {
8888
TypeParameter: "nfs",
8989
},
9090
},
91-
want: &ZFSStorageClassParameters{NFS: &NFSParameters{ShareProperties: "on"}},
91+
want: &ZFSStorageClassParameters{NFSShareProperties: "on"},
9292
},
9393
{
9494
name: "GivenCorrectSpec_WhenTypeHostPath_ThenReturnHostPathParameters",
@@ -100,7 +100,7 @@ func TestNewStorageClassParameters(t *testing.T) {
100100
NodeNameParameter: "my-node",
101101
},
102102
},
103-
want: &ZFSStorageClassParameters{HostPath: &HostPathParameters{NodeName: "my-node"}},
103+
want: &ZFSStorageClassParameters{HostPathNodeName: "my-node"},
104104
},
105105
}
106106
for _, tt := range tests {
@@ -112,8 +112,8 @@ func TestNewStorageClassParameters(t *testing.T) {
112112
return
113113
}
114114
assert.NoError(t, err)
115-
assert.Equal(t, tt.want.NFS, result.NFS)
116-
assert.Equal(t, tt.want.HostPath, result.HostPath)
115+
assert.Equal(t, tt.want.NFSShareProperties, result.NFSShareProperties)
116+
assert.Equal(t, tt.want.HostPathNodeName, result.HostPathNodeName)
117117
})
118118
}
119119
}

pkg/provisioner/provision.go

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package provisioner
33
import (
44
"context"
55
"fmt"
6+
"slices"
67
"strconv"
78

8-
"k8s.io/klog/v2"
9-
109
"github.com/ccremer/kubernetes-zfs-provisioner/pkg/zfs"
1110

1211
v1 "k8s.io/api/core/v1"
@@ -24,8 +23,9 @@ func (p *ZFSProvisioner) Provision(ctx context.Context, options controller.Provi
2423
datasetPath := fmt.Sprintf("%s/%s", parameters.ParentDataset, options.PVName)
2524
properties := make(map[string]string)
2625

27-
if parameters.NFS != nil {
28-
properties["sharenfs"] = parameters.NFS.ShareProperties
26+
useHostPath := canUseHostPath(parameters, options)
27+
if !useHostPath {
28+
properties[ShareNfsProperty] = parameters.NFSShareProperties
2929
}
3030

3131
var reclaimPolicy v1.PersistentVolumeReclaimPolicy
@@ -73,28 +73,44 @@ func (p *ZFSProvisioner) Provision(ctx context.Context, options controller.Provi
7373
},
7474
Spec: v1.PersistentVolumeSpec{
7575
PersistentVolumeReclaimPolicy: reclaimPolicy,
76-
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany, v1.ReadOnlyMany, v1.ReadWriteOnce},
76+
AccessModes: createAccessModes(options, useHostPath),
7777
Capacity: v1.ResourceList{
7878
v1.ResourceStorage: options.PVC.Spec.Resources.Requests[v1.ResourceStorage],
7979
},
80-
PersistentVolumeSource: createVolumeSource(parameters, dataset),
81-
NodeAffinity: createNodeAffinity(parameters),
80+
PersistentVolumeSource: createVolumeSource(parameters, dataset, useHostPath),
81+
NodeAffinity: createNodeAffinity(parameters, useHostPath),
8282
},
8383
}
8484
return pv, controller.ProvisioningFinished, nil
8585
}
8686

87-
func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Dataset) v1.PersistentVolumeSource {
88-
if parameters.NFS != nil {
89-
return v1.PersistentVolumeSource{
90-
NFS: &v1.NFSVolumeSource{
91-
Server: parameters.Hostname,
92-
Path: dataset.Mountpoint,
93-
ReadOnly: false,
94-
},
87+
func canUseHostPath(parameters *ZFSStorageClassParameters, options controller.ProvisionOptions) bool {
88+
switch parameters.Type {
89+
case Nfs:
90+
return false
91+
case HostPath:
92+
return true
93+
case Auto:
94+
if !slices.Contains(options.PVC.Spec.AccessModes, v1.ReadOnlyMany) && !slices.Contains(options.PVC.Spec.AccessModes, v1.ReadWriteMany) {
95+
return true
9596
}
9697
}
97-
if parameters.HostPath != nil {
98+
return false
99+
}
100+
101+
func createAccessModes(options controller.ProvisionOptions, useHostPath bool) []v1.PersistentVolumeAccessMode {
102+
if slices.Contains(options.PVC.Spec.AccessModes, v1.ReadWriteOncePod) {
103+
return []v1.PersistentVolumeAccessMode{v1.ReadWriteOncePod}
104+
}
105+
accessModes := []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}
106+
if !useHostPath {
107+
accessModes = append(accessModes, v1.ReadOnlyMany, v1.ReadWriteMany)
108+
}
109+
return accessModes
110+
}
111+
112+
func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Dataset, useHostPath bool) v1.PersistentVolumeSource {
113+
if useHostPath {
98114
hostPathType := v1.HostPathDirectory
99115
return v1.PersistentVolumeSource{
100116
HostPath: &v1.HostPathVolumeSource{
@@ -103,27 +119,34 @@ func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Data
103119
},
104120
}
105121
}
106-
klog.Exitf("Programmer error: Missing implementation for volume source: %v", parameters)
107-
return v1.PersistentVolumeSource{}
122+
123+
return v1.PersistentVolumeSource{
124+
NFS: &v1.NFSVolumeSource{
125+
Server: parameters.Hostname,
126+
Path: dataset.Mountpoint,
127+
ReadOnly: false,
128+
},
129+
}
108130
}
109131

110-
func createNodeAffinity(parameters *ZFSStorageClassParameters) *v1.VolumeNodeAffinity {
111-
if parameters.HostPath != nil {
112-
node := parameters.HostPath.NodeName
113-
if node == "" {
114-
node = parameters.Hostname
115-
}
116-
return &v1.VolumeNodeAffinity{Required: &v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
117-
{
118-
MatchExpressions: []v1.NodeSelectorRequirement{
119-
{
120-
Values: []string{node},
121-
Operator: v1.NodeSelectorOpIn,
122-
Key: v1.LabelHostname,
123-
},
132+
func createNodeAffinity(parameters *ZFSStorageClassParameters, useHostPath bool) *v1.VolumeNodeAffinity {
133+
if !useHostPath {
134+
return nil
135+
}
136+
137+
node := parameters.HostPathNodeName
138+
if node == "" {
139+
node = parameters.Hostname
140+
}
141+
return &v1.VolumeNodeAffinity{Required: &v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
142+
{
143+
MatchExpressions: []v1.NodeSelectorRequirement{
144+
{
145+
Values: []string{node},
146+
Operator: v1.NodeSelectorOpIn,
147+
Key: v1.LabelHostname,
124148
},
125149
},
126-
}}}
127-
}
128-
return nil
150+
},
151+
}}}
129152
}

pkg/provisioner/provision_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ func TestProvisionNfs(t *testing.T) {
5353
pv, _, err := p.Provision(context.Background(), options)
5454
require.NoError(t, err)
5555
assertBasics(t, stub, pv, expectedDatasetName, expectedHost)
56+
assert.Contains(t, pv.Spec.AccessModes, v1.ReadWriteOnce)
57+
// Pods located on other nodes can mount this PV
58+
assert.Contains(t, pv.Spec.AccessModes, v1.ReadOnlyMany)
59+
assert.Contains(t, pv.Spec.AccessModes, v1.ReadWriteMany)
5660

5761
assert.Equal(t, v1.PersistentVolumeReclaimDelete, pv.Spec.PersistentVolumeReclaimPolicy)
5862

@@ -66,10 +70,6 @@ func TestProvisionNfs(t *testing.T) {
6670
func assertBasics(t *testing.T, stub *zfsStub, pv *v1.PersistentVolume, expectedDataset string, expectedHost string) {
6771
stub.AssertExpectations(t)
6872

69-
assert.Contains(t, pv.Spec.AccessModes, v1.ReadWriteOnce)
70-
assert.Contains(t, pv.Spec.AccessModes, v1.ReadOnlyMany)
71-
assert.Contains(t, pv.Spec.AccessModes, v1.ReadWriteMany)
72-
7373
assert.Contains(t, pv.Annotations, "my/annotation")
7474
assert.Equal(t, expectedDataset, pv.Annotations[DatasetPathAnnotation])
7575
assert.Equal(t, expectedHost, pv.Annotations[ZFSHostAnnotation])
@@ -111,6 +111,10 @@ func TestProvisionHostPath(t *testing.T) {
111111
pv, _, err := p.Provision(context.Background(), options)
112112
require.NoError(t, err)
113113
assertBasics(t, stub, pv, expectedDatasetName, expectedHost)
114+
assert.Contains(t, pv.Spec.AccessModes, v1.ReadWriteOnce)
115+
// Pods located on other nodes cannot mount this PV
116+
assert.NotContains(t, pv.Spec.AccessModes, v1.ReadOnlyMany)
117+
assert.NotContains(t, pv.Spec.AccessModes, v1.ReadWriteMany)
114118

115119
assert.Equal(t, policy, pv.Spec.PersistentVolumeReclaimPolicy)
116120

pkg/provisioner/provisioner.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111

1212
RefQuotaProperty = "refquota"
1313
RefReservationProperty = "refreservation"
14+
ShareNfsProperty = "sharenfs"
1415
ManagedByProperty = "io.kubernetes.pv.zfs:managed_by"
1516
ReclaimPolicyProperty = "io.kubernetes.pv.zfs:reclaim_policy"
1617
)

0 commit comments

Comments
 (0)