Skip to content

Commit 0b57dff

Browse files
authored
feat(hostpath):enforce xfs quota on the hostpath (#78)
Initial implementation to enforce quota on the XFS volumes. - The feature is controlled by specifying the quota parameter in the StorageClass along with specifying the values for limits. Note: Additional PRs will be raised to refactor the implementation and add integration tests. Signed-off-by : Almas Ahmad <[email protected]>
1 parent 7fbbfb8 commit 0b57dff

File tree

3 files changed

+287
-3
lines changed

3 files changed

+287
-3
lines changed

cmd/provisioner-localpv/app/helper_hostpath.go

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ package app
2121

2222
import (
2323
"context"
24+
"math"
2425
"path/filepath"
26+
"regexp"
27+
"strconv"
2528
"time"
2629

2730
errors "github.com/pkg/errors"
@@ -69,12 +72,21 @@ type HelperPodOptions struct {
6972
//path is the volume hostpath directory
7073
path string
7174

72-
// serviceAccountName is the service account with which the pod should be launched
75+
//serviceAccountName is the service account with which the pod should be launched
7376
serviceAccountName string
7477

7578
selectedNodeTaints []corev1.Taint
7679

7780
imagePullSecrets []corev1.LocalObjectReference
81+
82+
//softLimitGrace is the soft limit of quota on the project
83+
softLimitGrace string
84+
85+
//hardLimitGrace is the hard limit of quota on the project
86+
hardLimitGrace string
87+
88+
//pvcStorage is the storage requested for pv
89+
pvcStorage int64
7890
}
7991

8092
// validate checks that the required fields to launch
@@ -92,6 +104,64 @@ func (pOpts *HelperPodOptions) validate() error {
92104
return nil
93105
}
94106

107+
//validateLimits check that the limits to setup qouta are valid
108+
func (pOpts *HelperPodOptions) validateLimits() error {
109+
if pOpts.softLimitGrace == "0k" &&
110+
pOpts.hardLimitGrace == "0k" {
111+
return errors.Errorf("both limits cannot be 0")
112+
}
113+
114+
if pOpts.softLimitGrace == "0k" ||
115+
pOpts.hardLimitGrace == "0k" {
116+
return nil
117+
}
118+
119+
if len(pOpts.softLimitGrace) > len(pOpts.hardLimitGrace) ||
120+
(len(pOpts.softLimitGrace) == len(pOpts.hardLimitGrace) &&
121+
pOpts.softLimitGrace > pOpts.hardLimitGrace) {
122+
return errors.Errorf("hard limit cannot be smaller than soft limit")
123+
}
124+
125+
return nil
126+
}
127+
128+
//converToK converts the limits to kilobytes
129+
func convertToK(limit string, pvcStorage int64) (string, error) {
130+
131+
if len(limit) == 0 {
132+
return "0k", nil
133+
}
134+
135+
valueRegex := regexp.MustCompile(`[\d]*[\.]?[\d]*`)
136+
valueString := valueRegex.FindString(limit)
137+
138+
if limit != valueString+"%" {
139+
return "", errors.Errorf("invalid format for limit grace")
140+
}
141+
142+
value, err := strconv.ParseFloat(valueString, 64)
143+
144+
if err != nil {
145+
return "", errors.Errorf("invalid format, cannot parse")
146+
}
147+
if value > 100 {
148+
value = 100
149+
}
150+
151+
value *= float64(pvcStorage)
152+
value /= 100
153+
value += float64(pvcStorage)
154+
value /= 1000
155+
156+
if value != math.Trunc(value) {
157+
value++
158+
}
159+
value = math.Trunc(value)
160+
valueString = strconv.FormatFloat(value, 'f', -1, 64)
161+
valueString += "k"
162+
return valueString, nil
163+
}
164+
95165
// createInitPod launches a helper(busybox) pod, to create the host path.
96166
// The local pv expect the hostpath to be already present before mounting
97167
// into pod. Validate that the local pv host path is not created under root.
@@ -117,6 +187,8 @@ func (p *Provisioner) createInitPod(ctx context.Context, pOpts *HelperPodOptions
117187
//Pass on the taints, to create tolerations.
118188
config.taints = pOpts.selectedNodeTaints
119189

190+
config.pOpts.cmdsForPath = append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))
191+
120192
iPod, err := p.launchPod(ctx, config)
121193
if err != nil {
122194
return err
@@ -142,7 +214,6 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
142214
return err
143215
}
144216

145-
config.taints = pOpts.selectedNodeTaints
146217
// Initialize HostPath builder and validate that
147218
// volume directory is not directly under root.
148219
// Extract the base path and the volume unique path.
@@ -154,6 +225,10 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
154225
return vErr
155226
}
156227

228+
config.taints = pOpts.selectedNodeTaints
229+
230+
config.pOpts.cmdsForPath = append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))
231+
157232
cPod, err := p.launchPod(ctx, config)
158233
if err != nil {
159234
return err
@@ -165,6 +240,72 @@ func (p *Provisioner) createCleanupPod(ctx context.Context, pOpts *HelperPodOpti
165240
return nil
166241
}
167242

243+
// createQuotaPod launches a helper(busybox) pod, to apply the quota.
244+
// The local pv expect the hostpath to be already present before mounting
245+
// into pod. Validate that the local pv host path is not created under root.
246+
func (p *Provisioner) createQuotaPod(ctx context.Context, pOpts *HelperPodOptions) error {
247+
var config podConfig
248+
config.pOpts, config.podName = pOpts, "quota"
249+
//err := pOpts.validate()
250+
if err := pOpts.validate(); err != nil {
251+
return err
252+
}
253+
254+
// Initialize HostPath builder and validate that
255+
// volume directory is not directly under root.
256+
// Extract the base path and the volume unique path.
257+
var vErr error
258+
config.parentDir, config.volumeDir, vErr = hostpath.NewBuilder().WithPath(pOpts.path).
259+
WithCheckf(hostpath.IsNonRoot(), "volume directory {%v} should not be under root directory", pOpts.path).
260+
ExtractSubPath()
261+
if vErr != nil {
262+
return vErr
263+
}
264+
265+
//Pass on the taints, to create tolerations.
266+
config.taints = pOpts.selectedNodeTaints
267+
268+
var lErr error
269+
config.pOpts.softLimitGrace, lErr = convertToK(config.pOpts.softLimitGrace, config.pOpts.pvcStorage)
270+
if lErr != nil {
271+
return lErr
272+
}
273+
config.pOpts.hardLimitGrace, lErr = convertToK(config.pOpts.hardLimitGrace, config.pOpts.pvcStorage)
274+
if lErr != nil {
275+
return lErr
276+
}
277+
278+
if err := pOpts.validateLimits(); err != nil {
279+
return err
280+
}
281+
282+
//fs stores the file system of mount
283+
fs := "FS=`stat -f -c %T /data` ; "
284+
//check if fs is xfs
285+
checkXfs := "if [[ \"$FS\" != \"xfs\" ]]; then rm -rf " + filepath.Join("/data/", config.volumeDir) + " ;exit 1 ; else "
286+
//lastPid finds last project Id in the directory
287+
lastPid := "PID=`xfs_quota -x -c 'report -h' /data | tail -2 | awk 'NR==1{print substr ($1,2)}+0'` ;"
288+
//newPid increments last project Id by 1
289+
newPid := "PID=`expr $PID + 1` ;"
290+
//initializeProject initializes project with newpid
291+
initializeProject := "xfs_quota -x -c 'project -s -p " + filepath.Join("/data/", config.volumeDir) + " '$PID /data ;"
292+
//setQuota sets the quota according to limits defined
293+
setQuota := "xfs_quota -x -c 'limit -p bsoft=" + config.pOpts.softLimitGrace + " bhard=" + config.pOpts.hardLimitGrace + " '$PID /data ; fi"
294+
295+
config.pOpts.cmdsForPath = []string{"sh", "-c", fs + checkXfs + lastPid + newPid + initializeProject + setQuota}
296+
297+
qPod, err := p.launchPod(ctx, config)
298+
if err != nil {
299+
return err
300+
}
301+
302+
if err := p.exitPod(ctx, qPod); err != nil {
303+
return err
304+
}
305+
306+
return nil
307+
}
308+
168309
func (p *Provisioner) launchPod(ctx context.Context, config podConfig) (*corev1.Pod, error) {
169310
// the helper pod need to be launched in privileged mode. This is because in CoreOS
170311
// nodes, pods without privileged access cannot write to the host directory.
@@ -182,7 +323,7 @@ func (p *Provisioner) launchPod(ctx context.Context, config podConfig) (*corev1.
182323
container.NewBuilder().
183324
WithName("local-path-" + config.podName).
184325
WithImage(p.helperImage).
185-
WithCommandNew(append(config.pOpts.cmdsForPath, filepath.Join("/data/", config.volumeDir))).
326+
WithCommandNew(config.pOpts.cmdsForPath).
186327
WithVolumeMountsNew([]corev1.VolumeMount{
187328
{
188329
Name: "data",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
Copyright 2019 The OpenEBS Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
This code was taken from https://github.com/rancher/local-path-provisioner
17+
and modified to work with the configuration options used by OpenEBS
18+
*/
19+
20+
package app
21+
22+
import (
23+
"testing"
24+
)
25+
26+
func TestConvertToK(t *testing.T) {
27+
type args struct {
28+
limit string
29+
pvcStorage int64
30+
}
31+
tests := map[string]struct {
32+
name string
33+
args args
34+
want string
35+
wantErr bool
36+
}{
37+
"Missing limit grace": {
38+
args: args{
39+
limit: "",
40+
pvcStorage: 5000000000,
41+
},
42+
want: "0k",
43+
wantErr: false,
44+
},
45+
"Present limit with grace": {
46+
args: args{
47+
limit: "0%",
48+
pvcStorage: 5000,
49+
},
50+
want: "5k",
51+
wantErr: false,
52+
},
53+
"Present limit grace exceeding 100%": {
54+
args: args{
55+
limit: "200%",
56+
pvcStorage: 5000000,
57+
},
58+
want: "10000k",
59+
wantErr: false,
60+
},
61+
"Present limit grace with decimal%": {
62+
args: args{
63+
limit: ".5%",
64+
pvcStorage: 1000,
65+
},
66+
want: "2k",
67+
wantErr: false,
68+
},
69+
"Present limit grace with invalid pattern": {
70+
args: args{
71+
limit: "10",
72+
pvcStorage: 10000,
73+
},
74+
want: "",
75+
wantErr: true,
76+
},
77+
"Present limit grace with only %": {
78+
args: args{
79+
limit: "%",
80+
pvcStorage: 10000,
81+
},
82+
want: "",
83+
wantErr: true,
84+
},
85+
}
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
got, err := convertToK(tt.args.limit, tt.args.pvcStorage)
89+
if (err != nil) != tt.wantErr {
90+
t.Errorf("convertToK() error = %v, wantErr %v", err, tt.wantErr)
91+
return
92+
}
93+
if got != tt.want {
94+
t.Errorf("convertToK() = %v, want %v", got, tt.want)
95+
}
96+
})
97+
}
98+
}

cmd/provisioner-localpv/app/provisioner_hostpath.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ import (
3131
pvController "sigs.k8s.io/sig-storage-lib-external-provisioner/v7/controller"
3232
)
3333

34+
const (
35+
EnableXfsQuota string = "enableXfsQuota"
36+
SoftLimitGrace string = "softLimitGrace"
37+
HardLimitGrace string = "hardLimitGrace"
38+
)
39+
3440
// ProvisionHostPath is invoked by the Provisioner which expect HostPath PV
3541
// to be provisioned and a valid PV spec returned.
3642
func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.ProvisionOptions, volumeConfig *VolumeConfig) (*v1.PersistentVolume, pvController.ProvisioningState, error) {
@@ -87,6 +93,45 @@ func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.P
8793
return nil, pvController.ProvisioningFinished, iErr
8894
}
8995

96+
enableXfsQuota := opts.StorageClass.Parameters[EnableXfsQuota]
97+
98+
if enableXfsQuota == "true" {
99+
softLimitGrace := opts.StorageClass.Parameters[SoftLimitGrace]
100+
hardLimitGrace := opts.StorageClass.Parameters[HardLimitGrace]
101+
pvcStorage := opts.PVC.Spec.Resources.Requests.Storage().Value()
102+
103+
podOpts := &HelperPodOptions{
104+
name: name,
105+
path: path,
106+
nodeAffinityLabelKey: nodeAffinityKey,
107+
nodeAffinityLabelValue: nodeAffinityValue,
108+
serviceAccountName: saName,
109+
selectedNodeTaints: taints,
110+
imagePullSecrets: imagePullSecrets,
111+
softLimitGrace: softLimitGrace,
112+
hardLimitGrace: hardLimitGrace,
113+
pvcStorage: pvcStorage,
114+
}
115+
iErr := p.createQuotaPod(ctx, podOpts)
116+
if iErr != nil {
117+
klog.Infof("Applying quota failed: %v", iErr)
118+
alertlog.Logger.Errorw("",
119+
"eventcode", "local.pv.provision.failure",
120+
"msg", "Failed to provision Local PV",
121+
"rname", opts.PVName,
122+
"reason", "Quota enforcement failed",
123+
"storagetype", stgType,
124+
)
125+
return nil, pvController.ProvisioningFinished, iErr
126+
}
127+
alertlog.Logger.Infow("",
128+
"eventcode", "local.pv.quota.success",
129+
"msg", "Successfully applied quota",
130+
"rname", opts.PVName,
131+
"storagetype", stgType,
132+
)
133+
}
134+
90135
// VolumeMode will always be specified as Filesystem for host path volume,
91136
// and the value passed in from the PVC spec will be ignored.
92137
fs := v1.PersistentVolumeFilesystem

0 commit comments

Comments
 (0)