Skip to content

Commit

Permalink
Add "auto" provisioning type.
Browse files Browse the repository at this point in the history
Automatically use hostpath if pod is on same host as zfs pool.

This addresses ccremer#85. When storage class type is set to auto,
automatically create a hostpath volume when the scheduler selects the
specified node to run the pod, otherwise fallback to using NFS.

Note this only works when volumeBindingMode is set to
WaitForFirstConsumer in the storage class. Otherwise, when set to
Immediate, volumes will be pre-provisioned before the scheduler selects a
node for the pod consuming the volume, and options.SelectedNode will be
unset.

Note that there could be unintended side-effects if multiple pods using
the volume claim are scheduled on different nodes, depending on which
pods gets scheduled first. If the first pod gets scheduled on the ZFS
host, it will automatically use a hostpath volume and node affinity will
be set so that other pods will be prevented from running on other nodes.
On the other hand, if the second pod gets scheduled on a node which is
not the ZFS host, it will use a NFS volume, and subsequent pods will
also use NFS, even if scheduled to run on the ZFS host.
  • Loading branch information
jp39 committed Aug 3, 2024
1 parent 829d4a5 commit d70e9b7
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 58 deletions.
3 changes: 3 additions & 0 deletions charts/kubernetes-zfs-provisioner/templates/storageclass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ metadata:
{{ toYaml . | nindent 4 }}
{{- end }}
provisioner: {{ $.Values.provisioner.instance }}
{{- if eq .type "auto" }}
volumeBindingMode: WaitForFirstConsumer
{{- end }}
reclaimPolicy: {{ .policy | default "Delete" }}
parameters:
parentDataset: {{ .parentDataset }}
Expand Down
2 changes: 1 addition & 1 deletion charts/kubernetes-zfs-provisioner/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ storageClass:
# policy: "Delete"
# # -- NFS export properties (see `exports(5)`)
# shareProperties: ""
# # -- Provision type, one of [`nfs`, `hostpath`]
# # -- Provision type, one of [`nfs`, `hostpath`, `auto`]
# type: "nfs"
# # -- Override `kubernetes.io/hostname` from `hostName` parameter for
# # `HostPath` node affinity
Expand Down
47 changes: 30 additions & 17 deletions pkg/provisioner/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ const (
parameters:
parentDataset: tank/volumes
hostname: my-zfs-host.localdomain
type: nfs|hostpath
type: nfs|hostPath|auto
shareProperties: rw=10.0.0.0/8,no_root_squash
node: my-zfs-host
reserveSpace: true|false
*/

type ProvisioningType string

const (
Nfs ProvisioningType = "nfs"
HostPath ProvisioningType = "hostPath"
Auto ProvisioningType = "auto"
)

type (
// ZFSStorageClassParameters represents the parameters on the `StorageClass`
// object. It is used to ease access and validate those parameters at run time.
Expand All @@ -33,18 +41,13 @@ type (
ParentDataset string
// Hostname of the target ZFS host. Will be used to connect over SSH.
Hostname string
NFS *NFSParameters
HostPath *HostPathParameters
Type ProvisioningType
// NFSShareProperties specifies additional properties to pass to 'zfs create sharenfs=%s'.
NFSShareProperties string
// HostPathNodeName overrides the hostname if the Kubernetes node name is different than the ZFS target host. Used for Affinity
HostPathNodeName string
ReserveSpace bool
}
NFSParameters struct {
// ShareProperties specifies additional properties to pass to 'zfs create sharenfs=%s'.
ShareProperties string
}
HostPathParameters struct {
// NodeName overrides the hostname if the Kubernetes node name is different than the ZFS target host. Used for Affinity
NodeName string
}
)

// NewStorageClassParameters takes a storage class parameters, validates it for invalid configuration and returns a
Expand Down Expand Up @@ -79,16 +82,26 @@ func NewStorageClassParameters(parameters map[string]string) (*ZFSStorageClassPa
typeParam := parameters[TypeParameter]
switch typeParam {
case "hostpath", "hostPath", "HostPath", "Hostpath", "HOSTPATH":
p.HostPath = &HostPathParameters{NodeName: parameters[NodeNameParameter]}
return p, nil
p.Type = HostPath
case "nfs", "Nfs", "NFS":
p.Type = Nfs
case "auto", "Auto", "AUTO":
p.Type = Auto
default:
return nil, fmt.Errorf("invalid '%s' parameter value: %s", TypeParameter, typeParam)
}

if p.Type == HostPath || p.Type == Auto {
p.HostPathNodeName = parameters[NodeNameParameter]
}

if p.Type == Nfs || p.Type == Auto {
shareProps := parameters[SharePropertiesParameter]
if shareProps == "" {
shareProps = "on"
}
p.NFS = &NFSParameters{ShareProperties: shareProps}
return p, nil
default:
return nil, fmt.Errorf("invalid '%s' parameter value: %s", TypeParameter, typeParam)
p.NFSShareProperties = shareProps
}

return p, nil
}
10 changes: 5 additions & 5 deletions pkg/provisioner/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestNewStorageClassParameters(t *testing.T) {
SharePropertiesParameter: "rw",
},
},
want: &ZFSStorageClassParameters{NFS: &NFSParameters{ShareProperties: "rw"}},
want: &ZFSStorageClassParameters{NFSShareProperties: "rw"},
},
{
name: "GivenCorrectSpec_WhenTypeNfsWithoutProperties_ThenReturnNfsParametersWithDefault",
Expand All @@ -88,7 +88,7 @@ func TestNewStorageClassParameters(t *testing.T) {
TypeParameter: "nfs",
},
},
want: &ZFSStorageClassParameters{NFS: &NFSParameters{ShareProperties: "on"}},
want: &ZFSStorageClassParameters{NFSShareProperties: "on"},
},
{
name: "GivenCorrectSpec_WhenTypeHostPath_ThenReturnHostPathParameters",
Expand All @@ -100,7 +100,7 @@ func TestNewStorageClassParameters(t *testing.T) {
NodeNameParameter: "my-node",
},
},
want: &ZFSStorageClassParameters{HostPath: &HostPathParameters{NodeName: "my-node"}},
want: &ZFSStorageClassParameters{HostPathNodeName: "my-node"},
},
}
for _, tt := range tests {
Expand All @@ -112,8 +112,8 @@ func TestNewStorageClassParameters(t *testing.T) {
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want.NFS, result.NFS)
assert.Equal(t, tt.want.HostPath, result.HostPath)
assert.Equal(t, tt.want.NFSShareProperties, result.NFSShareProperties)
assert.Equal(t, tt.want.HostPathNodeName, result.HostPathNodeName)
})
}
}
68 changes: 34 additions & 34 deletions pkg/provisioner/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ func (p *ZFSProvisioner) Provision(ctx context.Context, options controller.Provi
datasetPath := fmt.Sprintf("%s/%s", parameters.ParentDataset, options.PVName)
properties := make(map[string]string)

if parameters.NFS != nil {
properties["sharenfs"] = parameters.NFS.ShareProperties
useHostPath := true
if parameters.Type == "nfs" || parameters.Type == "auto" && (options.SelectedNode == nil || parameters.HostPathNodeName != options.SelectedNode.Name) {
useHostPath = false
properties[ShareNfsProperty] = parameters.NFSShareProperties
}

var reclaimPolicy v1.PersistentVolumeReclaimPolicy
Expand Down Expand Up @@ -77,24 +79,15 @@ func (p *ZFSProvisioner) Provision(ctx context.Context, options controller.Provi
Capacity: v1.ResourceList{
v1.ResourceStorage: options.PVC.Spec.Resources.Requests[v1.ResourceStorage],
},
PersistentVolumeSource: createVolumeSource(parameters, dataset),
NodeAffinity: createNodeAffinity(parameters),
PersistentVolumeSource: createVolumeSource(parameters, dataset, useHostPath),
NodeAffinity: createNodeAffinity(parameters, useHostPath),
},
}
return pv, controller.ProvisioningFinished, nil
}

func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Dataset) v1.PersistentVolumeSource {
if parameters.NFS != nil {
return v1.PersistentVolumeSource{
NFS: &v1.NFSVolumeSource{
Server: parameters.Hostname,
Path: dataset.Mountpoint,
ReadOnly: false,
},
}
}
if parameters.HostPath != nil {
func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Dataset, useHostPath bool) v1.PersistentVolumeSource {
if useHostPath {
hostPathType := v1.HostPathDirectory
return v1.PersistentVolumeSource{
HostPath: &v1.HostPathVolumeSource{
Expand All @@ -103,27 +96,34 @@ func createVolumeSource(parameters *ZFSStorageClassParameters, dataset *zfs.Data
},
}
}
klog.Exitf("Programmer error: Missing implementation for volume source: %v", parameters)
return v1.PersistentVolumeSource{}

return v1.PersistentVolumeSource{
NFS: &v1.NFSVolumeSource{
Server: parameters.Hostname,
Path: dataset.Mountpoint,
ReadOnly: false,
},
}
}

func createNodeAffinity(parameters *ZFSStorageClassParameters) *v1.VolumeNodeAffinity {
if parameters.HostPath != nil {
node := parameters.HostPath.NodeName
if node == "" {
node = parameters.Hostname
}
return &v1.VolumeNodeAffinity{Required: &v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Values: []string{node},
Operator: v1.NodeSelectorOpIn,
Key: v1.LabelHostname,
},
func createNodeAffinity(parameters *ZFSStorageClassParameters, useHostPath bool) *v1.VolumeNodeAffinity {
if !useHostPath {
return nil
}

node := parameters.HostPathNodeName
if node == "" {
node = parameters.Hostname
}
return &v1.VolumeNodeAffinity{Required: &v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Values: []string{node},
Operator: v1.NodeSelectorOpIn,
Key: v1.LabelHostname,
},
},
}}}
}
return nil
},
}}}
}
1 change: 1 addition & 0 deletions pkg/provisioner/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (

RefQuotaProperty = "refquota"
RefReservationProperty = "refreservation"
ShareNfsProperty = "sharenfs"
ManagedByProperty = "io.kubernetes.pv.zfs:managed_by"
ReclaimPolicyProperty = "io.kubernetes.pv.zfs:reclaim_policy"
)
Expand Down
2 changes: 1 addition & 1 deletion pkg/zfs/zfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (z *zfsImpl) SetPermissions(dataset *Dataset) error {
if dataset.Mountpoint == "" {
return fmt.Errorf("undefined mountpoint for dataset: %s", dataset.Name)
}
cmd := exec.Command("update-permissions", dataset.Hostname, dataset.Mountpoint)
cmd := exec.Command("chmod", "g+w", dataset.Mountpoint)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("could not update permissions on '%s': %w: %s", dataset.Hostname, err, out)
Expand Down

0 comments on commit d70e9b7

Please sign in to comment.