Skip to content

Commit

Permalink
added validation of limits
Browse files Browse the repository at this point in the history
  • Loading branch information
almas33 committed Aug 11, 2021
1 parent 993a92a commit e42f2be
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 163 deletions.
142 changes: 138 additions & 4 deletions cmd/provisioner-localpv/app/helper_hostpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ package app

import (
"context"
"math"
"path/filepath"
"regexp"
"strconv"
"time"

errors "github.com/pkg/errors"
Expand All @@ -39,14 +42,16 @@ import (
type podConfig struct {
pOpts *HelperPodOptions
parentDir, volumeDir, podName string
bsoft, bhard string
taints []corev1.Taint
}

var (
//CmdTimeoutCounts specifies the duration to wait for cleanup pod
//to be launched.
CmdTimeoutCounts = 120

//conversion stores the equivalent of 1kb stored as 1kb, 1mb, 1gb, 1tb
conversion = []float64{1, 1024, 1048576, 1073741824}
)

// HelperPodOptions contains the options that
Expand All @@ -70,12 +75,18 @@ type HelperPodOptions struct {
//path is the volume hostpath directory
path string

// serviceAccountName is the service account with which the pod should be launched
//serviceAccountName is the service account with which the pod should be launched
serviceAccountName string

selectedNodeTaints []corev1.Taint

imagePullSecrets []corev1.LocalObjectReference

//bsoft is the soft limit of quota on the project
bsoft string

//bhard is the hard limit of quota on the project
bhard string
}

// validate checks that the required fields to launch
Expand All @@ -93,6 +104,63 @@ func (pOpts *HelperPodOptions) validate() error {
return nil
}

//validateLimits check that the limits to setup qouta are valid
func (pOpts *HelperPodOptions) validateLimits() error {
if pOpts.bsoft == "0k" &&
pOpts.bhard == "0k" {
return errors.Errorf("both limits cannot be 0")
}

if pOpts.bsoft == "0k" ||
pOpts.bhard == "0k" {
return nil
}

if len(pOpts.bsoft) > len(pOpts.bhard) ||
(len(pOpts.bsoft) == len(pOpts.bhard) &&
pOpts.bsoft > pOpts.bhard) {
return errors.Errorf("hard limit cannot be smaller than soft limit")
}

return nil
}

//converToK converts the limits to kilobytes
func convertToK(limit string) (string, error) {

if len(limit) == 0 {
return "0k", nil
}

valueRegex := regexp.MustCompile(`[\d]*[\.]?[\d]*`)
valueString := valueRegex.FindString(limit)
value, err := strconv.ParseFloat(valueString, 64)
if err != nil {
return "", errors.Errorf("invalid limit, cannot parse")
}

unitPresent := make([]bool, 4)

//if any of the size unit is present in the limit, it will
//mark index belonging to that size unit as true
unitPresent[3], err = regexp.MatchString(`[Tt].*`, limit)
unitPresent[2], err = regexp.MatchString(`[Gg].*`, limit)
unitPresent[1], err = regexp.MatchString(`[Mm].*`, limit)
unitPresent[0], err = regexp.MatchString(`[Kk].*`, limit)

for i := range unitPresent {
if unitPresent[i] {
value *= conversion[i]
value = math.Trunc(value)
valueString := strconv.FormatFloat(value, 'f', -1, 64)
valueString += "k"
return valueString, err
}
}

return "", errors.Errorf("limit size should be in kilobytes, megabytes, gigabytes or terabytes")
}

// createInitPod launches a helper(busybox) pod, to create the host path.
// The local pv expect the hostpath to be already present before mounting
// into pod. Validate that the local pv host path is not created under root.
Expand All @@ -118,6 +186,8 @@ func (p *Provisioner) createInitPod(ctx context.Context, pOpts *HelperPodOptions
//Pass on the taints, to create tolerations.
config.taints = pOpts.selectedNodeTaints

config.pOpts.cmdsForPath = append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))

iPod, err := p.launchPod(ctx, config)
if err != nil {
return err
Expand All @@ -143,7 +213,6 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
return err
}

config.taints = pOpts.selectedNodeTaints
// Initialize HostPath builder and validate that
// volume directory is not directly under root.
// Extract the base path and the volume unique path.
Expand All @@ -155,6 +224,10 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
return vErr
}

config.taints = pOpts.selectedNodeTaints

config.pOpts.cmdsForPath = append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))

cPod, err := p.launchPod(ctx, config)
if err != nil {
return err
Expand All @@ -166,6 +239,67 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
return nil
}

// createQuotaPod launches a helper(busybox) pod, to apply the quota.
// The local pv expect the hostpath to be already present before mounting
// into pod. Validate that the local pv host path is not created under root.
func (p *Provisioner) createQuotaPod(ctx context.Context, pOpts *HelperPodOptions) error {
var config podConfig
config.pOpts, config.podName = pOpts, "quota"
//err := pOpts.validate()
if err := pOpts.validate(); err != nil {
return err
}

// Initialize HostPath builder and validate that
// volume directory is not directly under root.
// Extract the base path and the volume unique path.
var vErr error
config.parentDir, config.volumeDir, vErr = hostpath.NewBuilder().WithPath(pOpts.path).
WithCheckf(hostpath.IsNonRoot(), "volume directory {%v} should not be under root directory", pOpts.path).
ExtractSubPath()
if vErr != nil {
return vErr
}

//Pass on the taints, to create tolerations.
config.taints = pOpts.selectedNodeTaints

var lErr error
config.pOpts.bsoft, lErr = convertToK(config.pOpts.bsoft)
if lErr != nil {
return lErr
}
config.pOpts.bhard, lErr = convertToK(config.pOpts.bhard)
if lErr != nil {
return lErr
}

if err := pOpts.validateLimits(); err != nil {
return err
}

//lastPid finds last project Id in the directory
lastPid := "PID=`xfs_quota -x -c 'report -h' /data | tail -2 | awk 'NR==1{print substr ($1,2)}+0'` ;"
//newPid increments last project Id by 1
newPid := "PID=`expr $PID + 1` ;"
//initializeProject
initializeProject := "xfs_quota -x -c 'project -s -p " + filepath.Join("/data/", config.volumeDir) + " '$PID /data ;"
setQuota := "xfs_quota -x -c 'limit -p bsoft=" + config.pOpts.bsoft + " bhard=" + config.pOpts.bhard + " '$PID /data"

config.pOpts.cmdsForPath = []string{"sh", "-c", lastPid + newPid + initializeProject + setQuota}

qPod, err := p.launchPod(ctx, config)
if err != nil {
return err
}

if err := p.exitPod(ctx, qPod); err != nil {
return err
}

return nil
}

func (p *Provisioner) launchPod(ctx context.Context, config podConfig) (*corev1.Pod, error) {
// the helper pod need to be launched in privileged mode. This is because in CoreOS
// nodes, pods without privileged access cannot write to the host directory.
Expand All @@ -183,7 +317,7 @@ func (p *Provisioner) launchPod(ctx context.Context, config podConfig) (*corev1.
container.NewBuilder().
WithName("local-path-" + config.podName).
WithImage(p.helperImage).
WithCommandNew(append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))).
WithCommandNew(config.pOpts.cmdsForPath).
WithVolumeMountsNew([]corev1.VolumeMount{
{
Name: "data",
Expand Down
46 changes: 38 additions & 8 deletions cmd/provisioner-localpv/app/provisioner_hostpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ import (
pvController "sigs.k8s.io/sig-storage-lib-external-provisioner/v7/controller"
)

const (
ScType string = "type"
Bsoft string = "bsoft"
Bhard string = "bhard"
)

// ProvisionHostPath is invoked by the Provisioner which expect HostPath PV
// to be provisioned and a valid PV spec returned.
func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.ProvisionOptions, volumeConfig *VolumeConfig) (*v1.PersistentVolume, pvController.ProvisioningState, error) {
Expand Down Expand Up @@ -87,17 +93,41 @@ func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.P
return nil, pvController.ProvisioningFinished, iErr
}

bsoft := opts.StorageClass.Parameters["bsoft"]
bhard := opts.StorageClass.Parameters["bhard"]
scType := opts.StorageClass.Parameters[ScType]

if bsoft != "" ||
bhard != "" {
if scType == "enforceXfsQuota" {
bsoft := opts.StorageClass.Parameters[Bsoft]
bhard := opts.StorageClass.Parameters[Bhard]

qErr := p.createInitQuotaPod(ctx, podOpts, bsoft, bhard)
if qErr != nil {
klog.Infof("Setting quota failed: %v", name, qErr)
return nil, pvController.ProvisioningFinished, qErr
podOpts := &HelperPodOptions{
name: name,
path: path,
nodeAffinityLabelKey: nodeAffinityKey,
nodeAffinityLabelValue: nodeAffinityValue,
serviceAccountName: saName,
selectedNodeTaints: taints,
imagePullSecrets: imagePullSecrets,
bsoft: bsoft,
bhard: bhard,
}
iErr := p.createQuotaPod(ctx, podOpts)
if iErr != nil {
klog.Infof("Applying quota failed: %v", iErr)
alertlog.Logger.Errorw("",
"eventcode", "local.pv.provision.failure",
"msg", "Failed to provision Local PV",
"rname", opts.PVName,
"reason", "Quota enforcement failed",
"storagetype", stgType,
)
return nil, pvController.ProvisioningFinished, iErr
}
alertlog.Logger.Infow("",
"eventcode", "local.pv.quota.success",
"msg", "Successfully applied quota",
"rname", opts.PVName,
"storagetype", stgType,
)
}

// VolumeMode will always be specified as Filesystem for host path volume,
Expand Down
Loading

0 comments on commit e42f2be

Please sign in to comment.