Commit f8661d62 authored by Robert Krawitz's avatar Robert Krawitz Committed by Robert Krawitz

Use xfs_quota command to apply quotas

parent 448e0c44
...@@ -446,7 +446,7 @@ const ( ...@@ -446,7 +446,7 @@ const (
// //
// Allow use of filesystems for ephemeral storage monitoring. // Allow use of filesystems for ephemeral storage monitoring.
// Only applies if LocalStorageCapacityIsolation is set. // Only applies if LocalStorageCapacityIsolation is set.
FSQuotaForLSCIMonitoring = "FSQuotaForLSCIMonitoring" LocalStorageCapacityIsolationFSQuotaMonitoring featuregate.Feature = "LocalStorageCapacityIsolationFSQuotaMonitoring"
) )
func init() { func init() {
...@@ -521,7 +521,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS ...@@ -521,7 +521,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
TTLAfterFinished: {Default: false, PreRelease: featuregate.Alpha}, TTLAfterFinished: {Default: false, PreRelease: featuregate.Alpha},
KubeletPodResources: {Default: false, PreRelease: featuregate.Alpha}, KubeletPodResources: {Default: false, PreRelease: featuregate.Alpha},
WindowsGMSA: {Default: false, PreRelease: featuregate.Alpha}, WindowsGMSA: {Default: false, PreRelease: featuregate.Alpha},
FSQuotaForLSCIMonitoring: {Default: false, PreRelease: utilfeature.Alpha}, LocalStorageCapacityIsolationFSQuotaMonitoring: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed // inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side: // unintentionally on either side:
......
...@@ -62,6 +62,7 @@ go_library( ...@@ -62,6 +62,7 @@ go_library(
"//pkg/kubelet/util/format:go_default_library", "//pkg/kubelet/util/format:go_default_library",
"//pkg/scheduler/api:go_default_library", "//pkg/scheduler/api:go_default_library",
"//pkg/scheduler/util:go_default_library", "//pkg/scheduler/util:go_default_library",
"//pkg/volume/util:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
......
...@@ -32,6 +32,7 @@ import ( ...@@ -32,6 +32,7 @@ import (
evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api" evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types" kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
schedulerutils "k8s.io/kubernetes/pkg/scheduler/util" schedulerutils "k8s.io/kubernetes/pkg/scheduler/util"
volumeutils "k8s.io/kubernetes/pkg/volume/util"
) )
const ( const (
...@@ -395,27 +396,11 @@ func podDiskUsage(podStats statsapi.PodStats, pod *v1.Pod, statsToMeasure []fsSt ...@@ -395,27 +396,11 @@ func podDiskUsage(podStats statsapi.PodStats, pod *v1.Pod, statsToMeasure []fsSt
}, nil }, nil
} }
func internalIsLocalEphemeralVolume(pod *v1.Pod, volume v1.Volume) bool {
return volume.GitRepo != nil ||
(volume.EmptyDir != nil && volume.EmptyDir.Medium != v1.StorageMediumMemory) ||
volume.ConfigMap != nil || volume.DownwardAPI != nil
}
// IsLocalEphemeralVolume determines whether a given volume name is ephemeral
func IsLocalEphemeralVolume(pod *v1.Pod, volumeName string) (bool, error) {
for _, volume := range pod.Spec.Volumes {
if volume.Name == volumeName {
return internalIsLocalEphemeralVolume(pod, volume), nil
}
}
return false, fmt.Errorf("Volume %s not found in pod %v", volumeName, pod)
}
// localEphemeralVolumeNames returns the set of ephemeral volumes for the pod that are local // localEphemeralVolumeNames returns the set of ephemeral volumes for the pod that are local
func localEphemeralVolumeNames(pod *v1.Pod) []string { func localEphemeralVolumeNames(pod *v1.Pod) []string {
result := []string{} result := []string{}
for _, volume := range pod.Spec.Volumes { for _, volume := range pod.Spec.Volumes {
if internalIsLocalEphemeralVolume(pod, volume) { if volumeutils.IsLocalEphemeralVolume(volume) {
result = append(result, volume.Name) result = append(result, volume.Name)
} }
} }
......
...@@ -16,12 +16,12 @@ go_library( ...@@ -16,12 +16,12 @@ go_library(
deps = [ deps = [
"//pkg/api/v1/resource:go_default_library", "//pkg/api/v1/resource:go_default_library",
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/kubelet/eviction:go_default_library",
"//pkg/volume:go_default_library", "//pkg/volume:go_default_library",
"//pkg/volume/util:go_default_library", "//pkg/volume/util:go_default_library",
"//pkg/volume/util/operationexecutor:go_default_library", "//pkg/volume/util/operationexecutor:go_default_library",
"//pkg/volume/util/types:go_default_library", "//pkg/volume/util/types:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/klog:go_default_library",
......
...@@ -25,8 +25,8 @@ import ( ...@@ -25,8 +25,8 @@ import (
"sync" "sync"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
apiv1resource "k8s.io/kubernetes/pkg/api/v1/resource" apiv1resource "k8s.io/kubernetes/pkg/api/v1/resource"
limits "k8s.io/kubernetes/pkg/kubelet/eviction"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util" "k8s.io/kubernetes/pkg/volume/util"
"k8s.io/kubernetes/pkg/volume/util/operationexecutor" "k8s.io/kubernetes/pkg/volume/util/operationexecutor"
...@@ -165,7 +165,7 @@ type volumeToMount struct { ...@@ -165,7 +165,7 @@ type volumeToMount struct {
// desiredSizeLimit indicates the desired upper bound on the size of the volume // desiredSizeLimit indicates the desired upper bound on the size of the volume
// (if so implemented) // (if so implemented)
desiredSizeLimit int64 desiredSizeLimit *resource.Quantity
} }
// The pod object represents a pod that references the underlying volume and // The pod object represents a pod that references the underlying volume and
...@@ -232,15 +232,12 @@ func (dsw *desiredStateOfWorld) AddPodToVolume( ...@@ -232,15 +232,12 @@ func (dsw *desiredStateOfWorld) AddPodToVolume(
} }
if _, volumeExists := dsw.volumesToMount[volumeName]; !volumeExists { if _, volumeExists := dsw.volumesToMount[volumeName]; !volumeExists {
var sizeLimit int64 var sizeLimit *resource.Quantity
sizeLimit = 0 if volumeSpec.Volume != nil {
isLocal, _ := limits.IsLocalEphemeralVolume(pod, volumeSpec.Name()) if util.IsLocalEphemeralVolume(*volumeSpec.Volume) {
if isLocal { _, podLimits := apiv1resource.PodRequestsAndLimits(pod)
_, podLimits := apiv1resource.PodRequestsAndLimits(pod) ephemeralStorageLimit := podLimits[v1.ResourceEphemeralStorage]
ephemeralStorageLimit := podLimits[v1.ResourceEphemeralStorage] sizeLimit = resource.NewQuantity(ephemeralStorageLimit.Value(), resource.BinarySI)
sizeLimit = ephemeralStorageLimit.Value()
if sizeLimit == 0 {
sizeLimit = -1
} }
} }
dsw.volumesToMount[volumeName] = volumeToMount{ dsw.volumesToMount[volumeName] = volumeToMount{
......
...@@ -236,17 +236,20 @@ func (ed *emptyDir) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { ...@@ -236,17 +236,20 @@ func (ed *emptyDir) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
if err == nil { if err == nil {
volumeutil.SetReady(ed.getMetaDir()) volumeutil.SetReady(ed.getMetaDir())
} }
if mounterArgs.DesiredSize != 0 { if mounterArgs.DesiredSize != nil {
if hasQuotas, _ := quota.SupportsQuotas(ed.mounter, dir); hasQuotas { hasQuotas, err := quota.SupportsQuotas(ed.mounter, dir)
klog.V(3).Infof("emptydir trying to assign quota") if err != nil {
return fmt.Errorf("Unable to check for quota support on %s: %s", dir, err.Error())
}
if hasQuotas {
klog.V(4).Infof("emptydir trying to assign quota %v on %s", mounterArgs.DesiredSize, dir)
err := quota.AssignQuota(ed.mounter, dir, mounterArgs.PodUID, mounterArgs.DesiredSize) err := quota.AssignQuota(ed.mounter, dir, mounterArgs.PodUID, mounterArgs.DesiredSize)
if err != nil { if err != nil {
klog.V(3).Infof("Set quota failed %v", err) return fmt.Errorf("Set quota on %s failed %s", dir, err.Error())
} }
} }
} }
return nil
return err
} }
// setupTmpfs creates a tmpfs mount at the specified directory. // setupTmpfs creates a tmpfs mount at the specified directory.
...@@ -411,7 +414,7 @@ func (ed *emptyDir) teardownDefault(dir string) error { ...@@ -411,7 +414,7 @@ func (ed *emptyDir) teardownDefault(dir string) error {
// Remove any quota // Remove any quota
err := quota.ClearQuota(ed.mounter, dir) err := quota.ClearQuota(ed.mounter, dir)
if err != nil { if err != nil {
klog.V(3).Infof("Failed to clear quota on %s: %v", dir, err) klog.Warningf("Warning: Failed to clear quota on %s: %v", dir, err)
} }
// Renaming the directory is not required anymore because the operation executor // Renaming the directory is not required anymore because the operation executor
// now handles duplicate operations on the same volume // now handles duplicate operations on the same volume
......
...@@ -58,11 +58,13 @@ func FsInfo(path string) (int64, int64, int64, int64, int64, int64, error) { ...@@ -58,11 +58,13 @@ func FsInfo(path string) (int64, int64, int64, int64, int64, int64, error) {
// DiskUsage gets disk usage of specified path. // DiskUsage gets disk usage of specified path.
func DiskUsage(path string) (*resource.Quantity, error) { func DiskUsage(path string) (*resource.Quantity, error) {
// First check whether the quota system knows about this directory // First check whether the quota system knows about this directory
// A nil quantity with no error means that the path does not support quotas
// and we should use other mechanisms.
data, err := quota.GetConsumption(path) data, err := quota.GetConsumption(path)
if err == nil { if data != nil {
var q resource.Quantity return data, nil
q.Set(data) } else if err != nil {
return &q, nil return nil, fmt.Errorf("unable to retrieve disk consumption via quota for %s: %v", path, err)
} }
// Uses the same niceness level as cadvisor.fs does when running du // Uses the same niceness level as cadvisor.fs does when running du
// Uses -B 1 to always scale to a blocksize of 1 byte // Uses -B 1 to always scale to a blocksize of 1 byte
...@@ -85,9 +87,13 @@ func Find(path string) (int64, error) { ...@@ -85,9 +87,13 @@ func Find(path string) (int64, error) {
return 0, fmt.Errorf("invalid directory") return 0, fmt.Errorf("invalid directory")
} }
// First check whether the quota system knows about this directory // First check whether the quota system knows about this directory
// A nil quantity with no error means that the path does not support quotas
// and we should use other mechanisms.
inodes, err := quota.GetInodes(path) inodes, err := quota.GetInodes(path)
if err == nil { if inodes != nil {
return inodes, nil return inodes.Value(), nil
} else if err != nil {
return 0, fmt.Errorf("unable to retrieve inode consumption via quota for %s: %v", path, err)
} }
var counter byteCounter var counter byteCounter
var stderr bytes.Buffer var stderr bytes.Buffer
......
...@@ -25,6 +25,7 @@ go_library( ...@@ -25,6 +25,7 @@ go_library(
"//pkg/volume/util/volumepathhandler:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
"k8s.io/klog" "k8s.io/klog"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
...@@ -349,7 +350,7 @@ type VolumeToMount struct { ...@@ -349,7 +350,7 @@ type VolumeToMount struct {
// DesiredSizeLimit indicates the desired upper bound on the size of the volume // DesiredSizeLimit indicates the desired upper bound on the size of the volume
// (if so implemented) // (if so implemented)
DesiredSizeLimit int64 DesiredSizeLimit *resource.Quantity
} }
// GenerateMsgDetailed returns detailed msgs for volumes to mount // GenerateMsgDetailed returns detailed msgs for volumes to mount
......
...@@ -13,12 +13,11 @@ go_library( ...@@ -13,12 +13,11 @@ go_library(
deps = [ deps = [
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/util/mount:go_default_library", "//pkg/util/mount:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
] + select({ ] + select({
"@io_bazel_rules_go//go/platform:linux": [ "@io_bazel_rules_go//go/platform:linux": [
"//pkg/volume/util/quota/common:go_default_library", "//pkg/volume/util/quota/common:go_default_library",
"//pkg/volume/util/quota/extfs:go_default_library",
"//pkg/volume/util/quota/xfs:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//vendor/golang.org/x/sys/unix:go_default_library", "//vendor/golang.org/x/sys/unix:go_default_library",
"//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/klog:go_default_library",
...@@ -36,7 +35,9 @@ go_test( ...@@ -36,7 +35,9 @@ go_test(
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/util/mount:go_default_library", "//pkg/util/mount:go_default_library",
"//pkg/volume/util/quota/common:go_default_library", "//pkg/volume/util/quota/common:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
], ],
"//conditions:default": [], "//conditions:default": [],
}), }),
...@@ -54,8 +55,6 @@ filegroup( ...@@ -54,8 +55,6 @@ filegroup(
srcs = [ srcs = [
":package-srcs", ":package-srcs",
"//pkg/volume/util/quota/common:all-srcs", "//pkg/volume/util/quota/common:all-srcs",
"//pkg/volume/util/quota/extfs:all-srcs",
"//pkg/volume/util/quota/xfs:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
......
...@@ -6,12 +6,10 @@ go_library( ...@@ -6,12 +6,10 @@ go_library(
"quota_linux_common.go", "quota_linux_common.go",
"quota_linux_common_impl.go", "quota_linux_common_impl.go",
], ],
cgo = True,
importpath = "k8s.io/kubernetes/pkg/volume/util/quota/common", importpath = "k8s.io/kubernetes/pkg/volume/util/quota/common",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = select({ deps = select({
"@io_bazel_rules_go//go/platform:linux": [ "@io_bazel_rules_go//go/platform:linux": [
"//vendor/golang.org/x/sys/unix:go_default_library",
"//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/klog:go_default_library",
], ],
"//conditions:default": [], "//conditions:default": [],
......
...@@ -18,18 +18,46 @@ limitations under the License. ...@@ -18,18 +18,46 @@ limitations under the License.
package common package common
// QuotaID -- generic quota identifier import (
"regexp"
)
// QuotaID is generic quota identifier.
// Data type based on quotactl(2).
type QuotaID int32 type QuotaID int32
const ( const (
// UnknownQuotaID -- cannot determine whether a quota is in force
UnknownQuotaID QuotaID = -1
// BadQuotaID -- Invalid quota // BadQuotaID -- Invalid quota
BadQuotaID QuotaID = 0 BadQuotaID QuotaID = 0
) )
const (
acct = iota
enforcing = iota
)
// QuotaType -- type of quota to be applied
type QuotaType int
const (
// FSQuotaAccounting for quotas for accounting only
FSQuotaAccounting QuotaType = 1 << iota
// FSQuotaEnforcing for quotas for enforcement
FSQuotaEnforcing QuotaType = 1 << iota
)
// FirstQuota is the quota ID we start with. // FirstQuota is the quota ID we start with.
// XXXXXXX Need a better way of doing this... // XXXXXXX Need a better way of doing this...
var FirstQuota QuotaID = 1048577 var FirstQuota QuotaID = 1048577
// MountsFile is the location of the system mount data
var MountsFile = "/proc/self/mounts"
// MountParseRegexp parses out /proc/sys/self/mounts
var MountParseRegexp = regexp.MustCompilePOSIX("^([^ ]*)[ \t]*([^ ]*)[ \t]*([^ ]*)") // Ignore options etc.
// LinuxVolumeQuotaProvider returns an appropriate quota applier // LinuxVolumeQuotaProvider returns an appropriate quota applier
// object if we can support quotas on this device // object if we can support quotas on this device
type LinuxVolumeQuotaProvider interface { type LinuxVolumeQuotaProvider interface {
...@@ -59,7 +87,7 @@ type LinuxVolumeQuotaApplier interface { ...@@ -59,7 +87,7 @@ type LinuxVolumeQuotaApplier interface {
// Return value of false with no error means that the ID is not // Return value of false with no error means that the ID is not
// in use; true means that it is already in use. An error // in use; true means that it is already in use. An error
// return means that any quota ID will fail. // return means that any quota ID will fail.
QuotaIDIsInUse(path string, id QuotaID) (bool, error) QuotaIDIsInUse(id QuotaID) (bool, error)
// GetConsumption returns the consumption (in bytes) of the // GetConsumption returns the consumption (in bytes) of the
// directory, determined by the implementation's quota-based // directory, determined by the implementation's quota-based
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["quota_extfs.go"],
cgo = True,
importpath = "k8s.io/kubernetes/pkg/volume/util/quota/extfs",
visibility = ["//visibility:public"],
deps = select({
"@io_bazel_rules_go//go/platform:linux": [
"//pkg/volume/util/quota/common:go_default_library",
"//vendor/golang.org/x/sys/unix:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
"//conditions:default": [],
}),
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
// +build linux
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package extfs
/*
#include <stdlib.h>
#include <dirent.h>
#include <linux/fs.h>
#include <linux/quota.h>
#include <errno.h>
#ifndef PRJQUOTA
#define PRJQUOTA 2
#endif
#ifndef Q_SETPQUOTA
#define Q_SETPQUOTA (unsigned) QCMD(Q_SETQUOTA, PRJQUOTA)
#endif
#ifndef Q_GETPQUOTA
#define Q_GETPQUOTA (unsigned) QCMD(Q_GETQUOTA, PRJQUOTA)
#endif
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/volume/util/quota/common"
)
// ext4fs empirically has a maximum quota size of 2^48 - 1 1KiB blocks (256 petabytes)
const (
linuxExtfsMagic = 0xef53
quotaBsize = 1024 // extfs specific
bitsPerWord = 32 << (^uint(0) >> 63) // either 32 or 64
maxQuota int64 = (1<<(bitsPerWord-1) - 1) & (1<<58 - 1) // either 1<<31 - 1 or 1<<58 - 1
)
// VolumeProvider supplies a quota applier to the generic code.
type VolumeProvider struct {
}
// GetQuotaApplier -- does this backing device support quotas that
// can be applied to directories?
func (*VolumeProvider) GetQuotaApplier(mountpoint string, backingDev string) common.LinuxVolumeQuotaApplier {
if common.IsFilesystemOfType(mountpoint, backingDev, linuxExtfsMagic) {
return extfsVolumeQuota{backingDev}
}
return nil
}
type extfsVolumeQuota struct {
backingDev string
}
// GetQuotaOnDir -- get the quota ID that applies to this directory.
func (v extfsVolumeQuota) GetQuotaOnDir(path string) (common.QuotaID, error) {
return common.GetQuotaOnDir(path)
}
// SetQuotaOnDir -- apply the specified quota to the directory. If
// bytes is not greater than zero, the quota should be applied in a
// way that is non-enforcing (either explicitly so or by setting a
// quota larger than anything the user may possibly create)
func (v extfsVolumeQuota) SetQuotaOnDir(path string, id common.QuotaID, bytes int64) error {
klog.V(3).Infof("extfsSetQuotaOn %s ID %v bytes %v", path, id, bytes)
if bytes < 0 || bytes > maxQuota {
bytes = maxQuota
}
var d C.struct_if_dqblk
d.dqb_bhardlimit = C.__u64(bytes / quotaBsize)
d.dqb_bsoftlimit = d.dqb_bhardlimit
d.dqb_ihardlimit = 0
d.dqb_isoftlimit = 0
d.dqb_valid = C.QIF_LIMITS
var cs = C.CString(v.backingDev)
defer C.free(unsafe.Pointer(cs))
_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_SETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(id),
uintptr(unsafe.Pointer(&d)), 0, 0)
if errno != 0 {
return fmt.Errorf("Failed to set quota limit for ID %d on %s: %v",
id, path, errno.Error())
}
return common.ApplyProjectToDir(path, id)
}
func (v extfsVolumeQuota) getQuotaInfo(path string, id common.QuotaID) (C.struct_if_dqblk, syscall.Errno) {
var d C.struct_if_dqblk
var cs = C.CString(v.backingDev)
defer C.free(unsafe.Pointer(cs))
_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_GETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(id)),
uintptr(unsafe.Pointer(&d)), 0, 0)
return d, errno
}
// QuotaIDIsInUse -- determine whether the quota ID is already in use.
func (v extfsVolumeQuota) QuotaIDIsInUse(path string, id common.QuotaID) (bool, error) {
d, errno := v.getQuotaInfo(path, id)
isInUse := !(d.dqb_bhardlimit == 0 && d.dqb_bsoftlimit == 0 && d.dqb_curspace == 0 &&
d.dqb_ihardlimit == 0 && d.dqb_isoftlimit == 0 && d.dqb_curinodes == 0 &&
d.dqb_btime == 0 && d.dqb_itime == 0)
return errno == 0 && isInUse, nil
}
// GetConsumption -- retrieve the consumption (in bytes) of the directory
// Note that with ext[[:digit:]]fs the quota consumption is in bytes
// per man quotactl
func (v extfsVolumeQuota) GetConsumption(path string, id common.QuotaID) (int64, error) {
d, errno := v.getQuotaInfo(path, id)
if errno != 0 {
return 0, fmt.Errorf("Failed to get quota for %s: %s", path, errno.Error())
}
klog.V(3).Infof("Consumption for %s is %v", path, d.dqb_curspace)
return int64(d.dqb_curspace), nil
}
// GetInodes -- retrieve the number of inodes in use under the directory
func (v extfsVolumeQuota) GetInodes(path string, id common.QuotaID) (int64, error) {
d, errno := v.getQuotaInfo(path, id)
if errno != 0 {
return 0, fmt.Errorf("Failed to get quota for %s: %s", path, errno.Error())
}
klog.V(3).Infof("Inode consumption for %s is %v", path, d.dqb_curinodes)
return int64(d.dqb_curinodes), nil
}
...@@ -29,15 +29,14 @@ import ( ...@@ -29,15 +29,14 @@ import (
"sync" "sync"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/volume/util/quota/common" "k8s.io/kubernetes/pkg/volume/util/quota/common"
) )
var projectsFile = "/etc/projects" var projectsFile = "/etc/projects"
var projidFile = "/etc/projid" var projidFile = "/etc/projid"
var projectsParseRegexp *regexp.Regexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$") var projectsParseRegexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$")
var projidParseRegexp *regexp.Regexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$") var projidParseRegexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$")
var quotaIDLock sync.RWMutex var quotaIDLock sync.RWMutex
...@@ -78,12 +77,13 @@ func unlockFile(file *os.File) error { ...@@ -78,12 +77,13 @@ func unlockFile(file *os.File) error {
func openAndLockProjectFiles() (*os.File, *os.File, error) { func openAndLockProjectFiles() (*os.File, *os.File, error) {
// Make sure neither project-related file is a symlink! // Make sure neither project-related file is a symlink!
if err := projFilesAreOK(); err != nil { if err := projFilesAreOK(); err != nil {
return nil, nil, err return nil, nil, fmt.Errorf("system project files failed verification: %v", err)
} }
// We don't actually modify the original files; we create temporaries and // We don't actually modify the original files; we create temporaries and
// move them over the originals // move them over the originals
fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644) fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil { if err != nil {
err = fmt.Errorf("unable to open %s: %v", projectsFile, err)
return nil, nil, err return nil, nil, err
} }
fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644) fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644)
...@@ -97,10 +97,17 @@ func openAndLockProjectFiles() (*os.File, *os.File, error) { ...@@ -97,10 +97,17 @@ func openAndLockProjectFiles() (*os.File, *os.File, error) {
return fProjects, fProjid, nil return fProjects, fProjid, nil
} }
// Nothing useful we can do if we get an error here // Nothing useful we can do if we get an error here
err = fmt.Errorf("unable to lock %s: %v", projidFile, err)
unlockFile(fProjects) unlockFile(fProjects)
} else {
err = fmt.Errorf("unable to lock %s: %v", projectsFile, err)
} }
} else {
err = fmt.Errorf("system project files failed re-verification: %v", err)
} }
fProjid.Close() fProjid.Close()
} else {
err = fmt.Errorf("unable to open %s: %v", projidFile, err)
} }
fProjects.Close() fProjects.Close()
return nil, nil, err return nil, nil, err
...@@ -160,7 +167,7 @@ func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.Quot ...@@ -160,7 +167,7 @@ func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.Quot
unusedQuotasSearched := 0 unusedQuotasSearched := 0
for id := common.FirstQuota; id == id; id++ { for id := common.FirstQuota; id == id; id++ {
if _, ok := idMap[id]; !ok { if _, ok := idMap[id]; !ok {
isInUse, err := getApplier(path).QuotaIDIsInUse(path, id) isInUse, err := getApplier(path).QuotaIDIsInUse(id)
if err != nil { if err != nil {
return common.BadQuotaID, err return common.BadQuotaID, err
} else if !isInUse { } else if !isInUse {
...@@ -307,8 +314,7 @@ func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, l ...@@ -307,8 +314,7 @@ func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, l
} }
os.Remove(tmpProjects) os.Remove(tmpProjects)
} }
klog.V(3).Infof("Unable to write project files: %v", err) return fmt.Errorf("Unable to write project files: %v", err)
return err
} }
func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) { func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) {
...@@ -326,8 +332,7 @@ func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) { ...@@ -326,8 +332,7 @@ func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) {
} }
} }
} }
klog.V(3).Infof("addQuotaID %s %v failed %v", path, ID, err) return common.BadQuotaID, fmt.Errorf("createProjectID %s %v failed %v", path, ID, err)
return common.BadQuotaID, err
} }
func removeProjectID(path string, ID common.QuotaID) error { func removeProjectID(path string, ID common.QuotaID) error {
...@@ -348,6 +353,5 @@ func removeProjectID(path string, ID common.QuotaID) error { ...@@ -348,6 +353,5 @@ func removeProjectID(path string, ID common.QuotaID) error {
} }
} }
} }
klog.V(3).Infof("removeQuotaID %s %v failed %v", path, ID, err) return fmt.Errorf("removeProjectID %s %v failed %v", path, ID, err)
return err
} }
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package quota package quota
import ( import (
"k8s.io/apimachinery/pkg/api/resource"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
...@@ -28,13 +29,13 @@ type Interface interface { ...@@ -28,13 +29,13 @@ type Interface interface {
SupportsQuotas(m mount.Interface, path string) (bool, error) SupportsQuotas(m mount.Interface, path string) (bool, error)
// Assign a quota (picked by the quota mechanism) to a path, // Assign a quota (picked by the quota mechanism) to a path,
// and return it. // and return it.
AssignQuota(m mount.Interface, path string, poduid string, bytes int64) error AssignQuota(m mount.Interface, path string, poduid string, bytes *resource.Quantity) error
// Get the quota-based storage consumption for the path // Get the quota-based storage consumption for the path
GetConsumption(path string) (int64, error) GetConsumption(path string) (*resource.Quantity, error)
// Get the quota-based inode consumption for the path // Get the quota-based inode consumption for the path
GetInodes(path string) (int64, error) GetInodes(path string) (*resource.Quantity, error)
// Remove the quota from a path // Remove the quota from a path
// Implementations may assume that any data covered by the // Implementations may assume that any data covered by the
...@@ -43,5 +44,5 @@ type Interface interface { ...@@ -43,5 +44,5 @@ type Interface interface {
} }
func enabledQuotasForMonitoring() bool { func enabledQuotasForMonitoring() bool {
return utilfeature.DefaultFeatureGate.Enabled(features.FSQuotaForLSCIMonitoring) return utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolationFSQuotaMonitoring)
} }
...@@ -21,7 +21,9 @@ package quota ...@@ -21,7 +21,9 @@ package quota
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"k8s.io/apimachinery/pkg/api/resource"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/volume/util/quota/common" "k8s.io/kubernetes/pkg/volume/util/quota/common"
...@@ -74,13 +76,13 @@ func dummyFakeMount1() mount.Interface { ...@@ -74,13 +76,13 @@ func dummyFakeMount1() mount.Interface {
Opts: []string{"rw", "relatime"}, Opts: []string{"rw", "relatime"},
}, },
{ {
Device: "dev/mapper/fedora-root", Device: "/dev/mapper/fedora-root",
Path: "/", Path: "/",
Type: "ext4", Type: "ext4",
Opts: []string{"rw", "relatime"}, Opts: []string{"rw", "relatime"},
}, },
{ {
Device: "dev/mapper/fedora-home", Device: "/dev/mapper/fedora-home",
Path: "/home", Path: "/home",
Type: "ext4", Type: "ext4",
Opts: []string{"rw", "relatime"}, Opts: []string{"rw", "relatime"},
...@@ -363,7 +365,7 @@ func (v testVolumeQuota) GetQuotaOnDir(path string) (common.QuotaID, error) { ...@@ -363,7 +365,7 @@ func (v testVolumeQuota) GetQuotaOnDir(path string) (common.QuotaID, error) {
return common.BadQuotaID, fmt.Errorf("No quota available for %s", path) return common.BadQuotaID, fmt.Errorf("No quota available for %s", path)
} }
func (v testVolumeQuota) QuotaIDIsInUse(_ string, id common.QuotaID) (bool, error) { func (v testVolumeQuota) QuotaIDIsInUse(id common.QuotaID) (bool, error) {
if _, ok := testIDQuotaMap[id]; ok { if _, ok := testIDQuotaMap[id]; ok {
return true, nil return true, nil
} }
...@@ -389,7 +391,7 @@ func fakeSupportsQuotas(path string) (bool, error) { ...@@ -389,7 +391,7 @@ func fakeSupportsQuotas(path string) (bool, error) {
func fakeAssignQuota(path string, poduid string, bytes int64) error { func fakeAssignQuota(path string, poduid string, bytes int64) error {
dummySetFSInfo(path) dummySetFSInfo(path)
return AssignQuota(dummyQuotaTest(), path, poduid, bytes) return AssignQuota(dummyQuotaTest(), path, poduid, resource.NewQuantity(bytes, resource.DecimalSI))
} }
func fakeClearQuota(path string) error { func fakeClearQuota(path string) error {
...@@ -529,14 +531,6 @@ func compareProjectsFiles(t *testing.T, testcase quotaTestCase, projectsFile str ...@@ -529,14 +531,6 @@ func compareProjectsFiles(t *testing.T, testcase quotaTestCase, projectsFile str
} }
} }
func setFeature(feature utilfeature.Feature, value bool) error {
v := "true"
if !value {
v = "false"
}
return utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("%s=%s", string(feature), v))
}
func runCaseEnabled(t *testing.T, testcase quotaTestCase, seq int) bool { func runCaseEnabled(t *testing.T, testcase quotaTestCase, seq int) bool {
fail := false fail := false
var err error var err error
...@@ -604,9 +598,7 @@ func runCaseDisabled(t *testing.T, testcase quotaTestCase, seq int) bool { ...@@ -604,9 +598,7 @@ func runCaseDisabled(t *testing.T, testcase quotaTestCase, seq int) bool {
} }
func testAddRemoveQuotas(t *testing.T, enabled bool) { func testAddRemoveQuotas(t *testing.T, enabled bool) {
if err := setFeature(features.FSQuotaForLSCIMonitoring, enabled); err != nil { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LocalStorageCapacityIsolationFSQuotaMonitoring, enabled)()
t.Errorf("Unable to enable LSCI monitoring: %v", err)
}
tmpProjectsFile, err := ioutil.TempFile("", "projects") tmpProjectsFile, err := ioutil.TempFile("", "projects")
if err == nil { if err == nil {
_, err = tmpProjectsFile.WriteString(projectsHeader) _, err = tmpProjectsFile.WriteString(projectsHeader)
......
...@@ -20,6 +20,7 @@ package quota ...@@ -20,6 +20,7 @@ package quota
import ( import (
"errors" "errors"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
) )
...@@ -34,18 +35,18 @@ func SupportsQuotas(_ mount.Interface, _ string) (bool, error) { ...@@ -34,18 +35,18 @@ func SupportsQuotas(_ mount.Interface, _ string) (bool, error) {
} }
// AssignQuota -- dummy implementation // AssignQuota -- dummy implementation
func AssignQuota(_ mount.Interface, _ string, _ string, _ int64) error { func AssignQuota(_ mount.Interface, _ string, _ string, _ *resource.Quantity) error {
return errNotImplemented return errNotImplemented
} }
// GetConsumption -- dummy implementation // GetConsumption -- dummy implementation
func GetConsumption(_ string) (int64, error) { func GetConsumption(_ string) (*resource.Quantity, error) {
return 0, errNotImplemented return nil, errNotImplemented
} }
// GetInodes -- dummy implementation // GetInodes -- dummy implementation
func GetInodes(_ string) (int64, error) { func GetInodes(_ string) (*resource.Quantity, error) {
return 0, errNotImplemented return nil, errNotImplemented
} }
// ClearQuota -- dummy implementation // ClearQuota -- dummy implementation
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["quota_xfs.go"],
cgo = True,
importpath = "k8s.io/kubernetes/pkg/volume/util/quota/xfs",
visibility = ["//visibility:public"],
deps = select({
"@io_bazel_rules_go//go/platform:linux": [
"//pkg/volume/util/quota/common:go_default_library",
"//vendor/golang.org/x/sys/unix:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
"//conditions:default": [],
}),
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
// +build linux
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package xfs
/*
#include <stdlib.h>
#include <dirent.h>
#include <linux/fs.h>
#include <linux/quota.h>
#include <linux/dqblk_xfs.h>
#include <errno.h>
#ifndef PRJQUOTA
#define PRJQUOTA 2
#endif
#ifndef XFS_PROJ_QUOTA
#define XFS_PROJ_QUOTA 2
#endif
#ifndef Q_XSETPQLIM
#define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA)
#endif
#ifndef Q_XGETPQUOTA
#define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
#endif
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/volume/util/quota/common"
)
const (
linuxXfsMagic = 0x58465342
// Documented in man xfs_quota(8); not necessarily the same
// as the filesystem blocksize
quotaBsize = 512
bitsPerWord = 32 << (^uint(0) >> 63) // either 32 or 64
maxQuota int64 = 1<<(bitsPerWord-1) - 1 // either 1<<31 - 1 or 1<<63 - 1
)
// VolumeProvider supplies a quota applier to the generic code.
type VolumeProvider struct {
}
// GetQuotaApplier -- does this backing device support quotas that
// can be applied to directories?
func (*VolumeProvider) GetQuotaApplier(mountpoint string, backingDev string) common.LinuxVolumeQuotaApplier {
if common.IsFilesystemOfType(mountpoint, backingDev, linuxXfsMagic) {
return xfsVolumeQuota{backingDev}
}
return nil
}
type xfsVolumeQuota struct {
backingDev string
}
// GetQuotaOnDir -- get the quota ID that applies to this directory.
func (v xfsVolumeQuota) GetQuotaOnDir(path string) (common.QuotaID, error) {
return common.GetQuotaOnDir(path)
}
// SetQuotaOnDir -- apply the specified quota to the directory. If
// bytes is not greater than zero, the quota should be applied in a
// way that is non-enforcing (either explicitly so or by setting a
// quota larger than anything the user may possibly create)
func (v xfsVolumeQuota) SetQuotaOnDir(path string, id common.QuotaID, bytes int64) error {
klog.V(3).Infof("xfsSetQuotaOn %s ID %v bytes %v", path, id, bytes)
if bytes < 0 || bytes > maxQuota {
bytes = maxQuota
}
var d C.fs_disk_quota_t
d.d_version = C.FS_DQUOT_VERSION
d.d_id = C.__u32(id)
d.d_flags = C.XFS_PROJ_QUOTA
d.d_fieldmask = C.FS_DQ_BHARD
d.d_blk_hardlimit = C.__u64(bytes / quotaBsize)
d.d_blk_softlimit = d.d_blk_hardlimit
var cs = C.CString(v.backingDev)
defer C.free(unsafe.Pointer(cs))
_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM,
uintptr(unsafe.Pointer(cs)), uintptr(d.d_id),
uintptr(unsafe.Pointer(&d)), 0, 0)
if errno != 0 {
return fmt.Errorf("Failed to set quota limit for ID %d on %s: %v",
id, path, errno.Error())
}
return common.ApplyProjectToDir(path, id)
}
func (v xfsVolumeQuota) getQuotaInfo(path string, id common.QuotaID) (C.fs_disk_quota_t, syscall.Errno) {
var d C.fs_disk_quota_t
var cs = C.CString(v.backingDev)
defer C.free(unsafe.Pointer(cs))
_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(id)),
uintptr(unsafe.Pointer(&d)), 0, 0)
return d, errno
}
// QuotaIDIsInUse -- determine whether the quota ID is already in use.
func (v xfsVolumeQuota) QuotaIDIsInUse(path string, id common.QuotaID) (bool, error) {
_, errno := v.getQuotaInfo(path, id)
return errno == 0, nil
}
// GetConsumption -- retrieve the consumption (in bytes) of the directory
func (v xfsVolumeQuota) GetConsumption(path string, id common.QuotaID) (int64, error) {
d, errno := v.getQuotaInfo(path, id)
if errno != 0 {
return 0, fmt.Errorf("Failed to get quota for %s: %s", path, errno.Error())
}
klog.V(3).Infof("Consumption for %s is %v", path, d.d_bcount*quotaBsize)
return int64(d.d_bcount) * quotaBsize, nil
}
// GetInodes -- retrieve the number of inodes in use under the directory
func (v xfsVolumeQuota) GetInodes(path string, id common.QuotaID) (int64, error) {
d, errno := v.getQuotaInfo(path, id)
if errno != 0 {
return 0, fmt.Errorf("Failed to get quota for %s: %s", path, errno.Error())
}
klog.V(3).Infof("Inode consumption for %s is %v", path, d.d_icount)
return int64(d.d_icount), nil
}
...@@ -540,3 +540,11 @@ func GetPluginMountDir(host volume.VolumeHost, name string) string { ...@@ -540,3 +540,11 @@ func GetPluginMountDir(host volume.VolumeHost, name string) string {
mntDir := filepath.Join(host.GetPluginDir(name), MountsInGlobalPDPath) mntDir := filepath.Join(host.GetPluginDir(name), MountsInGlobalPDPath)
return mntDir return mntDir
} }
// IsLocalEphemeralVolume determines whether the argument is a local ephemeral
// volume vs. some other type
func IsLocalEphemeralVolume(volume v1.Volume) bool {
return volume.GitRepo != nil ||
(volume.EmptyDir != nil && volume.EmptyDir.Medium != v1.StorageMediumMemory) ||
volume.ConfigMap != nil || volume.DownwardAPI != nil
}
...@@ -104,7 +104,7 @@ type Attributes struct { ...@@ -104,7 +104,7 @@ type Attributes struct {
// MounterArgs provides more easily extensible arguments to Mounter // MounterArgs provides more easily extensible arguments to Mounter
type MounterArgs struct { type MounterArgs struct {
FsGroup *int64 FsGroup *int64
DesiredSize int64 DesiredSize *resource.Quantity
PodUID string PodUID string
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment