Repository: topolvm/topolvm
Pull request #851 (2024-03-06 12:07:40)

feat: Introduce minimum PVC allocation sizing by creating minimum allocation settings
This PR introduces a minimum size allocation for any PVCs that are requested. It also refactors the test cases to be table driven so that its easier to modify and see whats wrong.

The limits introduced are the following:

```
const (
	// DefaultMinimumAllocationSizeBlock is the default minimum size for a block volume.
	// Derived from the usual physical extent size of 4Mi * 2 (for accommodating metadata)
	DefaultMinimumAllocationSizeBlock = "8Mi"
	// DefaultMinimumAllocationSizeXFS is the default minimum size for a filesystem volume with XFS formatting.
	// Derived from the hard XFS minimum size of 300Mi that is enforced by the XFS filesystem.
	DefaultMinimumAllocationSizeXFS = "300Mi"
	// DefaultMinimumAllocationSizeExt4 is the default minimum size for a filesystem volume with ext4 formatting.
	// Derived from the usual 4096K blocks, 1024 inode default and journaling overhead,
	// Allows for more than 80% free space after formatting, anything lower significantly reduces this percentage.
	DefaultMinimumAllocationSizeExt4 = "32Mi"
)
```

**Why?**
Since we removed the 1Gi Allocation we should set sensible defaults that allow users to still make use of our rounding but that do not go so low as to break the Mounting procedure.

**What was changed?**

The following new controller flags were introduced with the above defaults:

- `minimum-allocation-block`
- `minimum-allocation-xfs`
- `minimum-allocation-ext4`

If not set, they use the defaults so the HELM chart does not need adjusting.
Note that setting 0 or negative values in these configurations disables the allocation sizing for that configuration.


**How to reproduce?**
Create a PVC with something like 

```
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: simple-pvc
spec:
  storageClassName: sg1 # should be with default or explicit xfs setting
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Mi
```

One will notice the following error during mounting

```
Events:
  Type     Reason       Age                 From               Message
  ----     ------       ----                ----               -------
  Normal   Scheduled    2m4s                default-scheduler  Successfully assigned default/test-pod to cnv-qe-infra-23.cnvqe2.lab.eng.rdu2.redhat.com
  Warning  FailedMount  59s (x8 over 2m4s)  kubelet            MountVolume.SetUp failed for volume "pvc-1bba5d94-aa42-4f8e-a75e-fe16debdbd9a" : rpc error: code = Internal desc = mount failed: volume=17c48e86-3a9b-4ef0-83c1-698e0d6fd7b5, error=format of disk "/dev/topolvm/17c48e86-3a9b-4ef0-83c1-698e0d6fd7b5" failed: type:("xfs") target:("/var/lib/kubelet/pods/8b4ae8e7-55c0-45d5-805b-02496d66b782/volumes/kubernetes.io~csi/pvc-1bba5d94-aa42-4f8e-a75e-fe16debdbd9a/mount") options:("nouuid,defaults") errcode:(exit status 1) output:(size 3072 of data subvolume is too small, minimum 4096 blocks
....  (cut for brevity)
```

Additionally if one then increases that block amount to lets say 16 MiB one will eventually face (with a recent enough version of mkfs):

```
Events:
  Type     Reason       Age                 From               Message
  ----     ------       ----                ----               -------
  Normal   Scheduled    2m4s                default-scheduler  Successfully assigned default/test-pod to cnv-qe-infra-23.cnvqe2.lab.eng.rdu2.redhat.com
  Warning  FailedMount  59s (x8 over 2m4s)  kubelet            MountVolume.SetUp failed for volume "pvc-1bba5d94-aa42-4f8e-a75e-fe16debdbd9a" : rpc error: code = Internal desc = mount failed: volume=17c48e86-3a9b-4ef0-83c1-698e0d6fd7b5, error=format of disk "/dev/topolvm/17c48e86-3a9b-4ef0-83c1-698e0d6fd7b5" failed: type:("xfs") target:("/var/lib/kubelet/pods/8b4ae8e7-55c0-45d5-805b-02496d66b782/volumes/kubernetes.io~csi/pvc-1bba5d94-aa42-4f8e-a75e-fe16debdbd9a/mount") options:("nouuid,defaults") errcode:(exit status 1) output:(Filesystem must be larger than 300MB.)
....  (cut for brevity)
```

due to the change here: https://www.spinics.net/lists/linux-xfs/msg63099.html & https://www.spinics.net/lists/linux-xfs/msg63831.html


satoru-takeuchi: IMO, this kind of problem should be handled in the filesystem layer. Since this problem seems not to happen in the latest mkfs.xfs, I'd like to keep the current code as simple as possibile than applying this PR. @llamerada-jp How do you think?
jakobmoellerdev: Just to make clear that this will happen in the latest versions of mkfs with 300 MiB. Before it would happen with 16 MiB and the Filesystem would be almost unusable. I can also introduce an E2E case to confirm this
jakobmoellerdev: Also could you kindly tell what you mean with the "filesystem layer"? AFAIK the only alternative to the rounding would be to resize the volume during the mount procedure. 

A Pod using a PVC with the StorageClass is completely unaware of this limitation.
llamerada-jp: Originally, the minimum allocation size was 1 GiB. Therefore, I do not object to setting a new minimum allocation. However, we may avoid hard-coding these numbers. Some sites may not like to be allocated without their permission, and other file systems may have similar limitations. I do not want to make similar changes every time that happens. Therefore, how about specifying this number in the argument of the program or storage class? Is this an excessive function?
jakobmoellerdev: @llamerada-jp I am not opposed to that idea, I think this is a good point. My only question would be is how we should set this up. From what I understand the only way to give dynamic sizing configuration would be either through a new set of flags/env variables, or through a new custom configuration in lvmd (in which case however we wouldn't work through the CSI spec). We are currently missing detailed ways to configure sizing limitations in the gRPC controller logic.

I am open to ideas here. I would like to keep the configuration ability to configure not only flat limits, but also per VolumeMode/Type for specific Filesystems. 

I am also open to discuss a Design Doc and am willing to take the implementation, just need to know what preferences we have here.
llamerada-jp: > I am open to ideas here. I would like to keep the configuration ability to configure not only flat limits, but also per VolumeMode/Type for specific Filesystems.

It seems to be better to set it to storage class. Since fstype has already been set in the storage class, I think it is enough to add the storage class parameter as follows.
```
parameters:
  csi.storage.k8s.io/fstype: xfs
  topolvm.io/minimum-allocate(or minimum-size?): 300Mi
```
jakobmoellerdev: Good point. Let me accomodate this change so that we can use it dynamically.
llamerada-jp: I found a little problem when using the StorageClass to set new configurations: we cannot edit the existing SC. we have to recreate the SC with the same name for adding the configuration. I believe it is only a small limitation since we can continuously use existing PV/PVC. 
jakobmoellerdev: I think this tradeoff needs to be made clear in the docs for downstreams like OpenShift, but for TopoLVM simply documenting this should be fine.
jakobmoellerdev: I have a patch incoming for this PR but I don't have the E2E test yet, will take a bit more time
satoru-takeuchi: @jakobmoellerdev 

> Also could you kindly tell what you mean with the "filesystem layer"?

Oops, sorry. I meant filesystem "tool" layer, in this case, `mkfs.xfs`. In addition, I misunderstood the purpose of this PR. I considered that it is to stop the creation of too small volume.

I agree with adding a new parameter to SC as Yuji said.

> I have a patch incoming for this PR but I don't have the E2E test yet, will take a bit more time

I'll review the updated version, thanks.
jakobmoellerdev: Note to reviewers: Added the limitation generically since users might also want minimum sizes for block volumes
jakobmoellerdev: @llamerada-jp @satoru-takeuchi I have switched to using `minimum-allocated-size-filesystem` and `minimum-allocated-size-block` because the same storageclass might be used for both VolumeMode Filesystem and Block but the user might only want to limit Filesystem Volumes, not block storage. Thus we need to have these 2 flags. I also added some unit tests and made it so that negative capacities would result in 0 minimum. Please have another look
llamerada-jp: >  using minimum-allocated-size-filesystem and minimum-allocated-size-block because the same storageclass might be used for both VolumeMode Filesystem and Block but the user might only want to limit Filesystem Volumes, not block storage

I disagree with this specification. In this case, administrators should set up two storage classes because these are different specifications.
jakobmoellerdev: @llamerada-jp I'm not sure why a separate StorageClass is necessary for this. It is a fact that StorageClasses are used for abstraction over the Volume provisioning. If you force downstreams to create two storage classes, all users on the cluster will have to have the knowledge about which StorageClass to use in which scenario, e.g. when they specify the PVC they will have to know about which StorageClass is "safe" for their use case, or otherwise provisioning can fail.

How would cluster user (that is not maintaining the deployment of the CSIDriver or StorageClass) know that a storageclass `ssd-with-filesystem-limit` should be used over `ssd-with-block-limit` ? 

It's in the kubernetes StorageClass specification that a StorageClass can and should be able to support both modes (and we do this right now). Why would a minimum allocation size suddenly change that?

Just because XFS PVCs need to be limited to 300MiB doesn't mean that the administrator should be forced to create 2 StorageClasses if he wants to allow Block Storage under this amount IMO.

This will ultimately be a real consumption problem in LVMS as well, because we currently create exactly one StorageClass for a VolumeGroup, which works out for users well because they know that the mapping is 1:1. If we rely on 2 StorageClasses, consumption gets confusing and I would like to avoid issues with that. 

I originally agreed to your suggestion to accommodate dynamic limits but now already existing users cannot migrate their storageClass safely because its parameters are immutable, which will force us to do an API change in LVMS. I don't want to make consumption of this protection from broken mounts even harder to be honest.
llamerada-jp: @jakobmoellerdev 
Do you think it is better to specify it in the controller's arguments instead of the storage class? I suppose it would be better. Let us decide first whether to adopt the controller's arguments or storage class.

> but now already existing users cannot migrate their storageClass safely because its parameters are immutable, which will force us to do an API change in LVMS. I don't want to make consumption of this protection from broken mounts even harder to be honest.

I took this lightly, but there are probably many administrators and sites that cannot accept this risk.
llamerada-jp: @jakobmoellerdev 
The use of storage class may be excessive for this issue. I suppose using the controller's arguments might be better than using SC to resolve the original issue. I apologize as you have already done the coding. What do you think? 

> but now already existing users cannot migrate their storageClass safely because its parameters are immutable, which will force us to do an API change in LVMS. I don't want to make consumption of this protection from broken mounts even harder to be honest.

I took this lightly, but there are probably many administrators and sites that cannot accept this risk.
jakobmoellerdev: No worries, I agree after that discussion that a controller arg makes sense and I appreciate you walking through this with me! I think here I am facing 2 issues again though:

1. Would it be okay for you to create 2 flags for the controller configuration? Or should we even consider an environment variable or something similar so it can be mounted from a ConfigMap?
2. How would we integrate this into the E2E tests, we would need to restart the controller for that which I don't like
llamerada-jp: Thank you for your reply.

> Would it be okay for you to create 2 flags for the controller configuration?

How about specifying the minimum allocation size per xfs/ext4/btrfs/block using the controller's argument? Since most of TopoLVM's existing parameters utilize arguments. ConfigMap and environment values can be used as arguments.

> How would we integrate this into the E2E tests, we would need to restart the controller for that which I don't like

My idea is to prepare an environment with a minimum allocation size for xfs of 300 MiB, for a block size of 100 MiB, and ext4 none using arguments. How about you create PVCs in this environment and check the actual size allocated?
jakobmoellerdev: > How about specifying the minimum allocation size per xfs/ext4/btrfs/block using the controller's argument? Since most of TopoLVM's existing parameters utilize arguments. ConfigMap and environment values can be used as arguments.

That works for me, I will adjust

> My idea is to prepare an environment with a minimum allocation size for xfs of 300 MiB, for a block size of 100 MiB, and ext4 none using arguments. How about you create PVCs in this environment and check the actual size allocated?

What do you mean with environment? I would like to have an e2e Test running for this and the only way I know how to do this right now is by introducing a new set of helm values in the e2e tests and then creating a new lane in github actions. I think this is a bit much for this test though. Ideally I would like to be able to test multiple settings of the helm chart in different e2e tests.
jakobmoellerdev: I had trouble specifying a configuration that was flexible enough (per device class and sensible defaults) in just command line flags, so I decided to incorporate a ConfigMap for the controller that can be used to specify more complex settings as well as flags. The flags always take precedence from the commandline. Please see the details in the PR description.

Let me know if this works out for you.
llamerada-jp: I think using ConfigMap and DeviceClass are overdoing for this issue. Wouldn't it be better to specify the minimum allocated size by the controller's argument this time?
When there are different OS versions for nodes in a cluster, administrators may think about changing the settings for each device class. However, in that case, I want to carefully consider the configuration method based on more specific use cases and requirements when you need it. I believe that it is good to discuss configuring it in detail again when actual requests arise.
jakobmoellerdev: Thanks for taking a look. There is a reason I am providing deviceClass specific overrides already. LVMS is primarily used on device edge (thus the original request to go lower than 1Gi) and they have custom tuned filesystem and block device settings on the OS. In these scenarios they will want to override the defaults we give (e.g. 100MiB default for block storage is a good candidate, same goes for 100MiB ext4). 

I would like to keep these overrides for this reason. If one feels like the deployment is too complex via this or wants the legacy behavior one can disable the configMap completely or not set any overrides which makes the configuration easy. If we want to provide users with a way to configure defaults without using the configMap and set the configMap default to false, that is also fine with me.

However, I want to make it clear that this ConfigMap also solves another issue that topolvm binaries have: Currently, all topolvm binaries except lvmd are only configurable via flags which goes against 12-Factor App Rules. We want to be able to transparently configure them.

Additionally, all of the PR behavior is tested in unit and E2E tests, so I do not believe there is a big chance of regression.

Please reconsider your decision after this as otherwise I will have no way in downstream to allow overrides for specific StorageClasses / deviceClasses.
llamerada-jp: Could you tell me what custom tuned filesystem and block device settings are? Does this mean the small sized fses/bdevs like < 1GiB?

IMO, the minimum allocation size is not necessary to set for each node and device class, but is OK to be a global value.
So I still consider implementing as controller's parameter is enough.

> 12-Factor App Rules

Do you mean the following?
https://12factor.net/config

I think the implication of this is to avoid hard-coding constants and make them externally injectable. I think the argument fits this rule. 
I consider the operation cost is small in both (a) changing command line parameters (modify Deployment and restart the corresponding pods) and (b) creating/changing ConfigMap(environment variables) and restarting Deployment.
jakobmoellerdev: @llamerada-jp could you give me a quick heads up on your opinion on what I said above? Just want to make sure this doesn't get lost.
llamerada-jp: @jakobmoellerdev 
We are discussing this internally and going to get back to you tomorrow.
llamerada-jp: > Sure: We have 2 different use cases for the differentiation by device Class:
> 1. Customers want to use LVM Block Partitions for VM bootstrapping and want to use dedicated partitions
> for BIOS/UEFI which are significantly smaller or larger based on OS so they want a separate set of minimums.
> At the same time they do not want to change the default cluster configuration.

I'd like to know about "a separate set of minimums" in detail. I wonder why minimum allocation size might be changed
for each deviceClass. In my understanding, the minimum allocation sizes are only for avoiding fs mount failure. So I still consider
that it's a global configuration exists for each fs. Could you tell me a specific case where the minimum size
differs from the global value (e.g. 300 MiB for XFS)? To be honest, I can't imagine such cases probably
because I am not familiar with edge computing.

> 2. Customers want a way to have a "way out" / Exception of the filesystem limitation if they format a partition
> with custom inodes and block counts (for xfs, they differ from the default block size and data sections, as well as agsize
> with older kernels and maxpct for the inode assignment; other factors include size of the log section).
> This is to support old legacy workloads that now get shifted into the cloud, as well as to hyperoptimize frequently used
> partitions that have to stay isolated from other partitions due to compliance and device concerns.

I guess what you really want to accomplish at long last is setting not only minimum size but also many fs parameters
like agsize and maxpct for each device class. Does it correct?

> I would like to see it implemented that way, if there is no chance I can convince you I would like to propose changing
> the implementation to put the config map default to false in the helm chart and to include 4 flags (default minimum
> for xfs,btrfs,ext4,block) and then use these for the tests.

Introducing a new ConfigMap only for testing is too much.
jakobmoellerdev: > I'd like to know about "a separate set of minimums" in detail. I wonder why minimum allocation size might be changed for each deviceClass. In my understanding, the minimum allocation sizes are only for avoiding fs mount failure. So I still consider that it's a global configuration exists for each fs. Could you tell me a specific case where the minimum size differs from the global value (e.g. 300 MiB for XFS)? To be honest, I can't imagine such cases probably because I am not familiar with edge computing.

I think you are referring to the fact that xfs configuration would be global. And that is true for one node. However if we have 2 deviceClasses for 2 different nodes or nodeSets, then this changes. If Node A has a different fs default than node B then I would have to use 2 different controller configurations.

However I did notice that I did not consider that most users who want custom formatting could just use block devices and format the devices themselves. A common way to optimize formatting of ext4 for example is through disabling certain options that are not necessary on multi-gig devices: `^has_journal,^uninit_bg,^ext_attr,^huge_file,^64bit`. This means that technically only deviceClass specifics for block devices would be necessary. And even I consider that a big edge case

> I guess what you really want to accomplish at long last is setting not only minimum size but also many fs parameters like agsize and maxpct for each device class. Does it correct?

Yes that is correct. But since TopoLVM cannot do this right now the workaround for users is to create block devices and do fs mount/unmount themselves

> Introducing a new ConfigMap only for testing is too much.

The ConfigMap (at least in my opinion) is a much better way to configure an application than using CLI flags for everything. This is why almost every major solution at some point starts offering filebased configuration instead of args, because args are hard to document and control/audit. But I see that we have trouble arguing for the value add here as well so let me push that to a separate PR so as not to block this effort here further.

With the above said:
1. Lets just add global defaults for now without config file like you suggested to appeal to your request to scope down the PR
2. Lets talk about custom fs options+minimum sizes in a separate issue?
satoru-takeuchi: Sorry for the late reply. I was on paid leave yesterday and the day before yesterday.

@llamerada-jp I agree with you.

@jakobmoellerdev Please update this PR in accordance with the following description.

> With the above said:
> 
> 1. Lets just add global defaults for now without config file like you suggested to appeal to your request to scope down the PR
> 2. Lets talk about custom fs options+minimum sizes in a separate issue?

FYI, in general, I agree with the following your opinion.

> The ConfigMap (at least in my opinion) is a much better way to configure an application than using CLI flags for everything. This is why almost every major solution at some point starts offering filebased configuration instead of args, because args are hard to document and control/audit. But I see that we have trouble arguing for the value add here as well so let me push that to a separate PR so as not to block this effort here further.

We afraid to introduce ConfigMap just for edge cases. I'm glad if the design proposal of "ConfigMap-based custom fs options+minimum sizes" is also beneficial for non-edge users.
jakobmoellerdev: Adjusted as discussed, PTAL again before I squash the commits. Thanks for the feedback!
satoru-takeuchi: I'll finish reviewing this PR tomorro or the next to tomorror.
llamerada-jp: @jakobmoellerdev @satoru-takeuchi 
I would like to ask how many default values should be used for ext4 and block. From my study I have found the following. I think 128 KiB or 4MiB would be better, how do you think?
- Specifying 1 KiB for PVC does not cause an error. However, 4MiB was allocated in my environment. This is because the size of LVM PE was 4MiB in my environment.
- The minimum PE size is 128 KiB, but it seems that 4 MiB is set in most Linux environments.

man vgcreate
```
     -s|--physicalextentsize Size[m|UNIT]
          Sets the physical extent size of PVs in the VG.  The value must be either a power of 2 of at least 1 sector (where
          the sector size is the largest sector size of the PVs currently used in the VG), or at least 128KiB.  Once this
          value has been set, it is difficult to change without recreating the VG, unless no extents need moving.
```

/etc/lvm/lvm.conf (ubuntu 22.04)
```
    # Configuration option allocation/physical_extent_size.
    # Default physical extent size in KiB to use for new VGs.
    # This configuration option has an automatic default value.
    # physical_extent_size = 4096
```

satoru-takeuchi: @llamerada-jp IMO, 4MiB is enough.
jakobmoellerdev: I believe we are not taking into consideration the usual default value of pv_min_size:

```
	# Configuration option devices/pv_min_size.
	# Minimum size in KiB of block devices which can be used as PVs.
	# In a clustered environment all nodes must use the same value.
	# Any value smaller than 512KiB is ignored. The previous built-in
	# value was 512.
	# This configuration option has an automatic default value.
	# pv_min_size = 2048
```

This would lead to a minimum of at least 512KiB, but effectively 2MiB for most environments. Also together with what @llamerada-jp said, we need at least a full extent size of 4MiB to cover most default lvm installations.

Additionally when trying with a device of exactly 4MiBs we will receive

```
root@mainframe:~# lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
loop0         7:0    0     4M  0 loop 

lvcreate -L4MiB testvg -n testlv
  Volume group "testvg" has insufficient free space (0 extents): 1 required.
```

Even if we increase it, ext4 will use up quite a bit of space for metadata:

```
sblk
NAME            MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
loop0             7:0    0     8M  0 loop 

lvs
  LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  testlv testvg -wi-a----- 4.00m

mkfs.ext4 /dev/testvg/testlv
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 4096 1k blocks and 1024 inodes

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

df -H
Filesystem                 Size  Used Avail Use% Mounted on
/dev/mapper/testvg-testlv  2.9M   48k  2.6M   2% /mnt/testlv
```
As you can see, for a 8MiB lv formatted on ext4 with default settings on Fedora 39, we only have a size of 2.9M with a usable size of 2.6M (due to FS metadata).

Thus I recommend at least a minimum of 16-32MiB to have actually the amount available for use that was requested. Going any lower than that will not expose the requested size as available storage, but a smaller percentage.
satoru-takeuchi: @jakobmoellerdev Thank you for your detailed explanation.

> Thus I recommend at least a minimum of 16-32MiB to have actually the amount available for use that was requested.

OK. Then let's make it 32 MiB for now.
jakobmoellerdev: @llamerada-jp @satoru-takeuchi 

Have now the following limits:

- XFS: 300Mi
- ext4: 32Mi
- block/raw: 8Mi
satoru-takeuchi: @jakobmoellerdev It's reasonable. I'll finish to review soon.
satoru-takeuchi: @jakobmoellerdev Please update PR title to reflect the latest changes. It no longer uses ConfigMap.

Patch:
diff --git a/cmd/topolvm-controller/app/root.go b/cmd/topolvm-controller/app/root.go
index e75979ad..aa4922f4 100644
--- a/cmd/topolvm-controller/app/root.go
+++ b/cmd/topolvm-controller/app/root.go
@@ -8,10 +8,25 @@ import (
 
 	"github.com/spf13/cobra"
 	"github.com/topolvm/topolvm"
+	"github.com/topolvm/topolvm/internal/driver"
+	"k8s.io/apimachinery/pkg/api/resource"
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 )
 
+const (
+	// DefaultMinimumAllocationSizeBlock is the default minimum size for a block volume.
+	// Derived from the usual physical extent size of 4Mi * 2 (for accommodating metadata)
+	DefaultMinimumAllocationSizeBlock = "8Mi"
+	// DefaultMinimumAllocationSizeXFS is the default minimum size for a filesystem volume with XFS formatting.
+	// Derived from the hard XFS minimum size of 300Mi that is enforced by the XFS filesystem.
+	DefaultMinimumAllocationSizeXFS = "300Mi"
+	// DefaultMinimumAllocationSizeExt4 is the default minimum size for a filesystem volume with ext4 formatting.
+	// Derived from the usual 4096K blocks, 1024 inode default and journaling overhead,
+	// Allows for more than 80% free space after formatting, anything lower significantly reduces this percentage.
+	DefaultMinimumAllocationSizeExt4 = "32Mi"
+)
+
 var config struct {
 	csiSocket                   string
 	metricsAddr                 string
@@ -28,6 +43,7 @@ var config struct {
 	leaderElectionRetryPeriod   time.Duration
 	skipNodeFinalize            bool
 	zapOpts                     zap.Options
+	controllerServerSettings    driver.ControllerServerSettings
 }
 
 var rootCmd = &cobra.Command{
@@ -69,6 +85,21 @@ func init() {
 	fs.DurationVar(&config.leaderElectionRetryPeriod, "leader-election-retry-period", 2*time.Second, "Duration the LeaderElector clients should wait between tries of actions.")
 	fs.BoolVar(&config.skipNodeFinalize, "skip-node-finalize", false, "skips automatic cleanup of PhysicalVolumeClaims when a Node is deleted")
 
+	driver.QuantityVar(fs, &config.controllerServerSettings.MinimumAllocationSettings.Block,
+		"minimum-allocation-block",
+		resource.MustParse(DefaultMinimumAllocationSizeBlock),
+		"Minimum Allocation Sizing for block storage. Logical Volumes will always be at least this big.")
+	config.controllerServerSettings.MinimumAllocationSettings.Filesystem = make(map[string]driver.Quantity)
+	for filesystem, minimum := range map[string]resource.Quantity{
+		"ext4": resource.MustParse(DefaultMinimumAllocationSizeExt4),
+		"xfs":  resource.MustParse(DefaultMinimumAllocationSizeXFS),
+	} {
+		config.controllerServerSettings.MinimumAllocationSettings.Filesystem[filesystem] = driver.NewQuantityFlagVar(fs,
+			fmt.Sprintf("minimum-allocation-%s", filesystem),
+			minimum,
+			fmt.Sprintf("Minimum Allocation Sizing for volumes with the %s filesystem. Logical Volumes will always be at least this big.", filesystem))
+	}
+
 	goflags := flag.NewFlagSet("klog", flag.ExitOnError)
 	klog.InitFlags(goflags)
 	config.zapOpts.BindFlags(goflags)
diff --git a/cmd/topolvm-controller/app/run.go b/cmd/topolvm-controller/app/run.go
index 27ac61fe..3c4a817b 100644
--- a/cmd/topolvm-controller/app/run.go
+++ b/cmd/topolvm-controller/app/run.go
@@ -137,7 +137,7 @@ func subMain() error {
 	// Add gRPC server to manager.
 	grpcServer := grpc.NewServer()
 	csi.RegisterIdentityServer(grpcServer, driver.NewIdentityServer(checker.Ready))
-	controllerSever, err := driver.NewControllerServer(mgr)
+	controllerSever, err := driver.NewControllerServer(mgr, config.controllerServerSettings)
 	if err != nil {
 		return err
 	}
diff --git a/go.mod b/go.mod
index 806f6398..113bb68a 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/prometheus/common v0.44.0
 	github.com/pseudomuto/protoc-gen-doc v1.5.0
 	github.com/spf13/cobra v1.7.0
+	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.17.0
 	go.uber.org/zap v1.25.0
 	golang.org/x/sys v0.15.0
@@ -96,7 +97,6 @@ require (
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.10.0 // indirect
 	github.com/spf13/cast v1.5.1 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stoewer/go-strcase v1.2.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	go.etcd.io/etcd/api/v3 v3.5.9 // indirect
diff --git a/internal/driver/allocation_settings.go b/internal/driver/allocation_settings.go
new file mode 100644
index 00000000..3fec40fe
--- /dev/null
+++ b/internal/driver/allocation_settings.go
@@ -0,0 +1,107 @@
+package driver
+
+import (
+	"reflect"
+
+	"github.com/container-storage-interface/spec/lib/go/csi"
+	"github.com/spf13/pflag"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+// MinimumAllocationSettings contains the minimum allocation settings for the controller.
+// It contains the default settings.
+type MinimumAllocationSettings struct {
+	Filesystem map[string]Quantity `json:"filesystem" ,yaml:"filesystem"`
+	Block      Quantity            `json:"block" ,yaml:"block"`
+}
+
+type Quantity resource.Quantity
+
+var _ pflag.Value = &Quantity{}
+
+func newQuantityValue(val resource.Quantity, p *Quantity) *Quantity {
+	*p = Quantity(val)
+	return p
+}
+
+func NewQuantityFlagVar(fs *pflag.FlagSet, name string, value resource.Quantity, usage string) Quantity {
+	p := new(Quantity)
+	fs.Var(newQuantityValue(value, p), name, usage)
+	return *p
+}
+
+func QuantityVar(fs *pflag.FlagSet, p *Quantity, name string, value resource.Quantity, usage string) {
+	fs.Var(newQuantityValue(value, p), name, usage)
+}
+
+func (q *Quantity) String() string {
+	rq := resource.Quantity(*q)
+	return rq.String()
+}
+
+func (q *Quantity) Set(s2 string) error {
+	rq, err := resource.ParseQuantity(s2)
+	if err != nil {
+		return err
+	}
+	*q = Quantity(rq)
+	return nil
+}
+
+func (q *Quantity) Type() string {
+	rq := resource.Quantity(*q)
+	return reflect.TypeOf(rq).String()
+}
+
+// MinMaxAllocationsFromSettings returns the minimum and maximum allocations based on the settings.
+// It uses the required and limit bytes from the CSI Call and the device class and capabilities from the StorageClass.
+// It then returns the minimum and maximum allocations in bytes that can be used for that context.
+func (settings MinimumAllocationSettings) MinMaxAllocationsFromSettings(
+	required, limit int64,
+	capabilities []*csi.VolumeCapability,
+) (int64, int64) {
+	minimumSize := settings.GetMinimumAllocationSize(capabilities)
+
+	if minimumSize.CmpInt64(required) > 0 {
+		ctrlLogger.Info("required size is less than minimum size, "+
+			"using minimum size as required size", "required", required, "minimum", minimumSize.Value())
+		required = minimumSize.Value()
+	}
+
+	return required, limit
+}
+
+// GetMinimumAllocationSize returns the minimum size to be allocated from the parameters derived from the StorageClass.
+// it uses either the filesystem or block key to get the minimum allocated size.
+// it determines which key to use based on the capabilities.
+// If no key is found or neither capability exists, it returns 0.
+// If the value is not a valid Quantity, it returns an error.
+func (settings MinimumAllocationSettings) GetMinimumAllocationSize(
+	capabilities []*csi.VolumeCapability,
+) resource.Quantity {
+	var quantity resource.Quantity
+
+	for _, capability := range capabilities {
+		if capability.GetBlock() != nil {
+			quantity = resource.Quantity(settings.Block)
+			break
+		}
+
+		if capability.GetMount() != nil {
+			rawQuantity, ok := settings.Filesystem[capability.GetMount().FsType]
+			if !ok {
+				break
+			}
+
+			quantity = resource.Quantity(rawQuantity)
+
+			break
+		}
+	}
+
+	if quantity.Sign() < 0 {
+		return resource.MustParse("0")
+	}
+
+	return quantity
+}
diff --git a/internal/driver/controller.go b/internal/driver/controller.go
index 81b49e72..04ef96be 100644
--- a/internal/driver/controller.go
+++ b/internal/driver/controller.go
@@ -20,8 +20,20 @@ import (
 
 var ctrlLogger = ctrl.Log.WithName("driver").WithName("controller")
 
+var (
+	ErrNoNegativeRequestBytes = errors.New("required capacity must not be negative")
+	ErrNoNegativeLimitBytes   = errors.New("capacity limit must not be negative")
+	ErrRequestedExceedsLimit  = errors.New("requested capacity exceeds limit capacity")
+	ErrResultingRequestIsZero = errors.New("requested capacity is 0")
+)
+
+// ControllerServerSettings hold all settings that should be passed to the controller server.
+type ControllerServerSettings struct {
+	MinimumAllocationSettings `json:"allocation" ,yaml:"allocation"`
+}
+
 // NewControllerServer returns a new ControllerServer.
-func NewControllerServer(mgr manager.Manager) (csi.ControllerServer, error) {
+func NewControllerServer(mgr manager.Manager, settings ControllerServerSettings) (csi.ControllerServer, error) {
 	lvService, err := k8s.NewLogicalVolumeService(mgr)
 	if err != nil {
 		return nil, err
@@ -33,6 +45,7 @@ func NewControllerServer(mgr manager.Manager) (csi.ControllerServer, error) {
 		server: &controllerServerNoLocked{
 			lvService:   lvService,
 			nodeService: k8s.NewNodeService(mgr.GetClient()),
+			settings:    settings,
 		},
 	}, nil
 }
@@ -109,6 +122,8 @@ type controllerServerNoLocked struct {
 
 	lvService   *k8s.LogicalVolumeService
 	nodeService *k8s.NodeService
+
+	settings ControllerServerSettings
 }
 
 func (s controllerServerNoLocked) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
@@ -139,6 +154,16 @@ func (s controllerServerNoLocked) CreateVolume(ctx context.Context, req *csi.Cre
 		return nil, status.Error(codes.InvalidArgument, "no volume capabilities are provided")
 	}
 
+	required, limit := s.settings.MinimumAllocationSettings.MinMaxAllocationsFromSettings(
+		req.GetCapacityRange().GetRequiredBytes(),
+		req.GetCapacityRange().GetLimitBytes(),
+		capabilities,
+	)
+
+	if err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "failed to calculate minimum/maximum allocation bytes: %v", err)
+	}
+
 	// check required volume capabilities
 	for _, capability := range capabilities {
 		if block := capability.GetBlock(); block != nil {
@@ -164,7 +189,7 @@ func (s controllerServerNoLocked) CreateVolume(ctx context.Context, req *csi.Cre
 		}
 	}
 
-	requestCapacityBytes, err := convertRequestCapacityBytes(req.GetCapacityRange().GetRequiredBytes(), req.GetCapacityRange().GetLimitBytes())
+	requestCapacityBytes, err := convertRequestCapacityBytes(required, limit)
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
@@ -418,16 +443,14 @@ func (s controllerServerNoLocked) DeleteSnapshot(ctx context.Context, req *csi.D
 // convertRequestCapacityBytes converts requestBytes and limitBytes to a valid capacity.
 func convertRequestCapacityBytes(requestBytes, limitBytes int64) (int64, error) {
 	if requestBytes < 0 {
-		return 0, errors.New("required capacity must not be negative")
+		return 0, ErrNoNegativeRequestBytes
 	}
 	if limitBytes < 0 {
-		return 0, errors.New("capacity limit must not be negative")
+		return 0, ErrNoNegativeLimitBytes
 	}
 
 	if limitBytes != 0 && requestBytes > limitBytes {
-		return 0, fmt.Errorf(
-			"requested capacity exceeds limit capacity: request=%d limit=%d", requestBytes, limitBytes,
-		)
+		return 0, fmt.Errorf("%w: request=%d limit=%d", ErrRequestedExceedsLimit, requestBytes, limitBytes)
 	}
 
 	// if requestBytes is 0 and
@@ -443,8 +466,8 @@ func convertRequestCapacityBytes(requestBytes, limitBytes int64) (int64, error)
 
 		roundedLimit := roundDown(limitBytes, topolvm.MinimumSectorSize)
 		if roundedLimit == 0 {
-			return 0, fmt.Errorf("requested capacity is 0, because it defaulted to the limit (%d) and was rounded down to the nearest sector size (%d). "+
-				"specify the limit to be at least %d bytes", limitBytes, topolvm.MinimumSectorSize, topolvm.MinimumSectorSize)
+			return 0, fmt.Errorf("%w, because it defaulted to the limit (%d) and was rounded down to the nearest sector size (%d). "+
+				"specify the limit to be at least %d bytes", ErrResultingRequestIsZero, limitBytes, topolvm.MinimumSectorSize, topolvm.MinimumSectorSize)
 		}
 
 		return roundedLimit, nil
@@ -456,9 +479,8 @@ func convertRequestCapacityBytes(requestBytes, limitBytes int64) (int64, error)
 		// after rounding up, we might overshoot the limit
 		if limitBytes > 0 && requestBytes > limitBytes {
 			return 0, fmt.Errorf(
-				"requested capacity rounded to nearest sector size (%d) exceeds limit capacity, "+
-					"either specify a lower request or a higher limit: request=%d limit=%d",
-				topolvm.MinimumSectorSize, requestBytes, limitBytes,
+				"%w, either specify a lower request or a higher limit (derived from %d sector size): request=%d limit=%d",
+				ErrRequestedExceedsLimit, topolvm.MinimumSectorSize, requestBytes, limitBytes,
 			)
 		}
 	}
@@ -624,7 +646,10 @@ func (s controllerServerNoLocked) ControllerExpandVolume(ctx context.Context, re
 		return nil, status.Error(codes.Internal, err.Error())
 	}
 
-	requestCapacityBytes, err := convertRequestCapacityBytes(req.GetCapacityRange().GetRequiredBytes(), req.GetCapacityRange().GetLimitBytes())
+	requestCapacityBytes, err := convertRequestCapacityBytes(
+		req.GetCapacityRange().GetRequiredBytes(),
+		req.GetCapacityRange().GetLimitBytes(),
+	)
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
diff --git a/internal/driver/node.go b/internal/driver/node.go
index 0d67b651..99f44a9e 100644
--- a/internal/driver/node.go
+++ b/internal/driver/node.go
@@ -482,23 +482,15 @@ func (s *nodeServerNoLocked) NodeExpandVolume(ctx context.Context, req *csi.Node
 
 	// We need to check the capacity range but don't use the converted value
 	// because the filesystem can be resized without the requested size.
-	_, err := convertRequestCapacityBytes(req.GetCapacityRange().GetRequiredBytes(), req.GetCapacityRange().GetLimitBytes())
+	_, err := convertRequestCapacityBytes(
+		req.GetCapacityRange().GetRequiredBytes(),
+		req.GetCapacityRange().GetLimitBytes(),
+	)
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
 
-	// Device type (block or fs, fs type detection) checking will be removed after CSI v1.2.0
-	// because `volume_capability` field will be added in csi.NodeExpandVolumeRequest
-	info, err := os.Stat(volumePath)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil, status.Errorf(codes.NotFound, "volume path is not exist: %s", volumePath)
-		}
-		return nil, status.Errorf(codes.Internal, "stat failed for %s: %v", volumePath, err)
-	}
-
-	isBlock := !info.IsDir()
-	if isBlock {
+	if isBlock := req.GetVolumeCapability().GetBlock() != nil; isBlock {
 		nodeLogger.Info("NodeExpandVolume(block) is skipped",
 			"volume_id", volumeID,
 			"target_path", volumePath,
diff --git a/internal/lvmd/device_class_manager.go b/internal/lvmd/device_class_manager.go
index 55c67be2..57dfb9c5 100644
--- a/internal/lvmd/device_class_manager.go
+++ b/internal/lvmd/device_class_manager.go
@@ -9,8 +9,8 @@ import (
 	lvmdTypes "github.com/topolvm/topolvm/pkg/lvmd/types"
 )
 
-// ErrDeviceClassNotFound is returned when a VG or LV is not found.
-var ErrDeviceClassNotFound = errors.New("device-class not found")
+// ErrNotFound is returned when a VG or LV is not found.
+var ErrNotFound = errors.New("device-class not found")
 
 const (
 	defaultSpareGB = 10
@@ -148,7 +148,7 @@ func (m DeviceClassManager) DeviceClass(dcName string) (*lvmdTypes.DeviceClass,
 	if v, ok := m.deviceClassByName[dcName]; ok {
 		return v, nil
 	}
-	return nil, ErrDeviceClassNotFound
+	return nil, ErrNotFound
 }
 
 // FindDeviceClassByVGName returns the device-class with the volume group name
@@ -156,7 +156,7 @@ func (m DeviceClassManager) FindDeviceClassByVGName(vgName string) (*lvmdTypes.D
 	if v, ok := m.deviceClassByVGName[vgName]; ok {
 		return v, nil
 	}
-	return nil, ErrDeviceClassNotFound
+	return nil, ErrNotFound
 }
 
 // FindDeviceClassByThinPoolName returns the device-class with volume group and pool combination
@@ -165,5 +165,5 @@ func (m DeviceClassManager) FindDeviceClassByThinPoolName(vgName string, poolNam
 	if v, ok := m.deviceClassByThinPoolName[name]; ok {
 		return v, nil
 	}
-	return nil, ErrDeviceClassNotFound
+	return nil, ErrNotFound
 }
diff --git a/internal/lvmd/lvservice.go b/internal/lvmd/lvservice.go
index bfd64d1f..93cc7c9f 100644
--- a/internal/lvmd/lvservice.go
+++ b/internal/lvmd/lvservice.go
@@ -159,14 +159,14 @@ func (s *lvService) RemoveLV(ctx context.Context, req *proto.RemoveLVRequest) (*
 	}
 
 	vg, err := command.FindVolumeGroup(ctx, dc.VolumeGroup)
-	if errors.Is(err, command.ErrNotFound) {
+	if errors.Is(err, ErrNotFound) {
 		return nil, status.Errorf(codes.NotFound, "%s: %s", err.Error(), req.DeviceClass)
 	} else if err != nil {
 		logger.Error(err, "failed to get volume group", "name", dc.VolumeGroup)
 		return nil, err
 	}
 
-	if err := vg.RemoveVolume(ctx, req.GetName()); errors.Is(err, command.ErrNotFound) {
+	if err := vg.RemoveVolume(ctx, req.GetName()); errors.Is(err, ErrNotFound) {
 		return nil, status.Errorf(codes.NotFound, "%s: %s", err.Error(), req.DeviceClass)
 	} else if err != nil {
 		logger.Error(err, "failed to remove volume", "name", req.GetName())
diff --git a/internal/lvmd/vgservice.go b/internal/lvmd/vgservice.go
index 668738de..2a5f6181 100644
--- a/internal/lvmd/vgservice.go
+++ b/internal/lvmd/vgservice.go
@@ -163,8 +163,8 @@ func (s *vgService) send(server proto.VGService_WatchServer) error {
 
 		for _, pool := range pools {
 			dc, err := s.dcManager.FindDeviceClassByThinPoolName(vg.Name(), pool.Name())
-			// we either get nil or ErrDeviceClassNotFound
-			if errors.Is(err, ErrDeviceClassNotFound) {
+			// we either get nil or ErrNotFound
+			if errors.Is(err, ErrNotFound) {
 				continue
 			}
 
@@ -203,7 +203,7 @@ func (s *vgService) send(server proto.VGService_WatchServer) error {
 		}
 
 		dc, err := s.dcManager.FindDeviceClassByVGName(vg.Name())
-		if errors.Is(err, ErrDeviceClassNotFound) {
+		if errors.Is(err, ErrNotFound) {
 			continue
 		}
 


Test patch:
diff --git a/internal/driver/allocation_settings_test.go b/internal/driver/allocation_settings_test.go
new file mode 100644
index 00000000..0a15279f
--- /dev/null
+++ b/internal/driver/allocation_settings_test.go
@@ -0,0 +1,180 @@
+package driver
+
+import (
+	"testing"
+
+	"github.com/container-storage-interface/spec/lib/go/csi"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+func Test_MinimumAllocationSettings(t *testing.T) {
+	mockBlock := &csi.VolumeCapability{AccessType: &csi.VolumeCapability_Block{
+		Block: &csi.VolumeCapability_BlockVolume{},
+	}}
+	mockMount := &csi.VolumeCapability{AccessType: &csi.VolumeCapability_Mount{
+		Mount: &csi.VolumeCapability_MountVolume{FsType: "ext4"},
+	}}
+
+	type testBytes struct {
+		required, limit int64
+	}
+	testCases := []struct {
+		name string
+
+		settings MinimumAllocationSettings
+
+		capabilities []*csi.VolumeCapability
+
+		input    testBytes
+		expected testBytes
+	}{
+		{
+			name:     "no settings result in pass through",
+			settings: MinimumAllocationSettings{},
+			input: testBytes{
+				required: 1 << 30,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 1 << 30,
+				limit:    2 << 30,
+			},
+		},
+		{
+			name: "minimum should be applied for block storage",
+			settings: MinimumAllocationSettings{
+				Block: Quantity(resource.MustParse("1Gi")),
+			},
+			capabilities: []*csi.VolumeCapability{mockBlock},
+			input: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 1 << 30,
+				limit:    2 << 30,
+			},
+		},
+		{
+			name: "negative minimum should result in no minimum",
+			settings: MinimumAllocationSettings{
+				Block: Quantity(resource.MustParse("-1")),
+			},
+			capabilities: []*csi.VolumeCapability{mockBlock},
+			input: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+		},
+		{
+			name: "minimum should be ignored if request is bigger",
+			settings: MinimumAllocationSettings{
+				Block: Quantity(resource.MustParse("1Gi")),
+			},
+			capabilities: []*csi.VolumeCapability{mockBlock},
+			input: testBytes{
+				required: 2 << 30,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 2 << 30,
+				limit:    2 << 30,
+			},
+		},
+		{
+			name: "minimum should be applied for filesystem storage (if fs matches)",
+			settings: MinimumAllocationSettings{
+				Filesystem: map[string]Quantity{
+					"ext4": Quantity(resource.MustParse("1Gi")),
+				},
+			},
+			capabilities: []*csi.VolumeCapability{mockMount},
+			input: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 1 << 30,
+				limit:    2 << 30,
+			},
+		},
+		{
+			name: "unknown filesystem minimum should be ignored",
+			settings: MinimumAllocationSettings{
+				Filesystem: map[string]Quantity{
+					"foo": Quantity(resource.MustParse("1Gi")),
+				},
+			},
+			capabilities: []*csi.VolumeCapability{mockMount},
+			input: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+			expected: testBytes{
+				required: 0,
+				limit:    2 << 30,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			required, limit := tc.settings.MinMaxAllocationsFromSettings(
+				tc.input.required, tc.input.limit, tc.capabilities)
+
+			if required != tc.expected.required {
+				t.Errorf("expected minimum/required bytes to be %d, but got %d", tc.expected.required, required)
+			}
+			if limit != tc.expected.limit {
+				t.Errorf("expected minimum/required bytes to be %d, but got %d", tc.expected.limit, limit)
+			}
+		})
+	}
+}
+
+func TestQuantity_UnmarshalText(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected int64
+		err      bool
+	}{
+		{
+			name:     "valid Quantity",
+			input:    "1Gi",
+			expected: 1 << 30,
+		},
+		{
+			name:     "negative Quantity",
+			input:    "-1",
+			expected: -1,
+		},
+		{
+			name:     "invalid Quantity",
+			input:    "blub",
+			expected: 0,
+			err:      true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var q Quantity
+			err := q.Set(tc.input)
+			if tc.err && err == nil {
+				t.Errorf("expected error, but got none")
+			}
+			if !tc.err && err != nil {
+				t.Errorf("expected no error, but got %v", err)
+			}
+			resourceQuantity := resource.Quantity(q)
+			if resourceQuantity.CmpInt64(tc.expected) != 0 {
+				t.Errorf("expected %d, but got %s", tc.expected, resourceQuantity.String())
+			}
+		})
+	}
+}
diff --git a/internal/driver/controller_test.go b/internal/driver/controller_test.go
index b605fe17..2b32c518 100644
--- a/internal/driver/controller_test.go
+++ b/internal/driver/controller_test.go
@@ -1,6 +1,7 @@
 package driver
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 
@@ -8,94 +9,87 @@ import (
 )
 
 func Test_convertRequestCapacityBytes(t *testing.T) {
-	_, err := convertRequestCapacityBytes(-1, 10)
-	if err == nil {
-		t.Error("should be error")
-	}
-	if err.Error() != "required capacity must not be negative" {
-		t.Error("should report invalid required capacity")
-	}
-
-	_, err = convertRequestCapacityBytes(10, -1)
-	if err == nil {
-		t.Error("should be error")
-	}
-	if err.Error() != "capacity limit must not be negative" {
-		t.Error("should report invalid capacity limit")
-	}
-
-	_, err = convertRequestCapacityBytes(20, 10)
-	if err == nil {
-		t.Error("should be error")
-	}
-	if err.Error() != "requested capacity exceeds limit capacity: request=20 limit=10" {
-		t.Error("should report capacity limit exceeded")
-	}
-
-	v, err := convertRequestCapacityBytes(0, topolvm.MinimumSectorSize-1)
-	if err == nil {
-		t.Error("should be error")
-	}
-	if err.Error() != "requested capacity is 0, because it defaulted to the limit (4095) and was rounded down to the nearest sector size (4096). specify the limit to be at least 4096 bytes" {
-		t.Errorf("should error if rounded-down limit is 0: %d", v)
-	}
-
-	v, err = convertRequestCapacityBytes(0, topolvm.MinimumSectorSize+1)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != topolvm.MinimumSectorSize {
-		t.Errorf("should be nearest rounded down multiple of sector size if 0 is supplied and limit is larger than sector-size: %d", v)
-	}
-
-	v, err = convertRequestCapacityBytes(0, 2<<30)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != 1<<30 {
-		t.Errorf("should be at least 1 Gi requested by default if 0 is supplied: %d", v)
-	}
-
-	v, err = convertRequestCapacityBytes(1, 0)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != topolvm.MinimumSectorSize {
-		t.Errorf("should be resolve capacities < 1Gi without error if there is no limit: %d", v)
-	}
-
-	v, err = convertRequestCapacityBytes(1<<30, 1<<30)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != 1<<30 {
-		t.Errorf("should be 1073741824 in byte precision: %d", v)
-	}
-
-	_, err = convertRequestCapacityBytes(1<<30+1, 1<<30+1)
-	if err == nil {
-		t.Error("should be error")
-	}
-	if err.Error() != "requested capacity rounded to nearest sector size (4096) exceeds limit capacity, either specify a lower request or a higher limit: request=1073745920 limit=1073741825" {
-		t.Error("should report capacity limit exceeded after rounding")
-	}
-
-	v, err = convertRequestCapacityBytes(0, 0)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != 1<<30 {
-		t.Errorf("should be 1073741825 in byte precision: %d", v)
+	testCases := []struct {
+		requestBytes int64
+		limitBytes   int64
+		expected     int64
+		err          error
+	}{
+		{
+			requestBytes: -1,
+			limitBytes:   10,
+			err:          ErrNoNegativeRequestBytes,
+		},
+		{
+			requestBytes: 10,
+			limitBytes:   -1,
+			err:          ErrNoNegativeLimitBytes,
+		},
+		{
+			requestBytes: 20,
+			limitBytes:   10,
+			err:          ErrRequestedExceedsLimit,
+		},
+		{
+			requestBytes: 1<<30 + 1,
+			limitBytes:   1<<30 + 1,
+			err:          ErrRequestedExceedsLimit,
+		},
+		{
+			requestBytes: 0,
+			limitBytes:   topolvm.MinimumSectorSize - 1,
+			err:          ErrResultingRequestIsZero,
+		},
+		{
+			requestBytes: 0,
+			limitBytes:   topolvm.MinimumSectorSize + 1,
+			expected:     topolvm.MinimumSectorSize,
+		},
+		{
+			requestBytes: 1,
+			limitBytes:   topolvm.MinimumSectorSize * 2,
+			expected:     topolvm.MinimumSectorSize,
+		},
+		{
+			requestBytes: 0,
+			limitBytes:   2 << 30,
+			expected:     1 << 30,
+		},
+		{
+			requestBytes: 1,
+			limitBytes:   0,
+			expected:     topolvm.MinimumSectorSize,
+		},
+		{
+			requestBytes: 1 << 30,
+			limitBytes:   1 << 30,
+			expected:     1 << 30,
+		},
+		{
+			requestBytes: 0,
+			limitBytes:   0,
+			expected:     1 << 30,
+		},
 	}
 
-	v, err = convertRequestCapacityBytes(1, topolvm.MinimumSectorSize*2)
-	if err != nil {
-		t.Error("should not be error")
-	}
-	if v != topolvm.MinimumSectorSize {
-		t.Errorf("should be %d in byte precision: %d", topolvm.MinimumSectorSize, v)
+	for _, tc := range testCases {
+		tcName := fmt.Sprintf("request:%d limit:%d", tc.requestBytes, tc.limitBytes)
+		if tc.err != nil {
+			tcName += fmt.Sprintf(" = %s", tc.err)
+		} else {
+			tcName += fmt.Sprintf(" = %v", tc.expected)
+		}
+
+		t.Run(tcName, func(t *testing.T) {
+			v, err := convertRequestCapacityBytes(tc.requestBytes, tc.limitBytes)
+			if !errors.Is(err, tc.err) {
+				t.Errorf("expected error %v, but got %v", tc.err, err)
+			}
+			if v != tc.expected {
+				t.Errorf("expected %d, but got %d", tc.expected, v)
+			}
+		})
 	}
-
 }
 
 func Test_roundUp(t *testing.T) {
diff --git a/internal/lvmd/device_class_manager_test.go b/internal/lvmd/device_class_manager_test.go
index fde99e2a..da79230a 100644
--- a/internal/lvmd/device_class_manager_test.go
+++ b/internal/lvmd/device_class_manager_test.go
@@ -433,7 +433,7 @@ func TestDeviceClassManager(t *testing.T) {
 	}
 
 	_, err = manager.DeviceClass("unknown")
-	if err != ErrDeviceClassNotFound {
+	if err != ErrNotFound {
 		t.Error("'unknown' should not be found")
 	}
 
@@ -446,7 +446,7 @@ func TestDeviceClassManager(t *testing.T) {
 	}
 
 	_, err = manager.FindDeviceClassByVGName("unknown")
-	if err != ErrDeviceClassNotFound {
+	if err != ErrNotFound {
 		t.Error("'unknown' should not be found")
 	}
 
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index 54a31958..f6d7e213 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -13,7 +13,9 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/topolvm/topolvm"
 	topolvmv1 "github.com/topolvm/topolvm/api/v1"
+	"github.com/topolvm/topolvm/cmd/topolvm-controller/app"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 )
 
 //go:embed testdata/e2e/pvc-template.yaml
@@ -57,7 +59,7 @@ func testE2E() {
 
 	It("should be mounted in specified path", func() {
 		By("deploying Pod with PVC")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1, "topolvm-provisioner"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1024, "topolvm-provisioner"))
 		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu", "topo-pvc"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
@@ -145,6 +147,100 @@ func testE2E() {
 		}).Should(Succeed())
 	})
 
+	It("should be mounted in specified path but changed by the minimum allocation default for XFS", func() {
+		By("deploying Pod with PVC")
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 100, "topolvm-provisioner"))
+		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu", "topo-pvc"))
+
+		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(podYaml, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("confirming that the specified device exists in the Pod")
+		Eventually(func() error {
+			_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "mountpoint", "-d", "/test1")
+			if err != nil {
+				return fmt.Errorf("failed to check mount point. err: %w", err)
+			}
+
+			stdout, err := kubectl("exec", "-n", ns, "ubuntu", "grep", "/test1", "/proc/mounts")
+			if err != nil {
+				return err
+			}
+			fields := strings.Fields(string(stdout))
+			if fields[2] != "xfs" {
+				return errors.New("/test1 is not xfs")
+			}
+			return nil
+		}).Should(Succeed())
+
+		By("writing file under /test1")
+		writePath := "/test1/bootstrap.log"
+		_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "cp", "/var/log/bootstrap.log", writePath)
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "sync")
+		Expect(err).ShouldNot(HaveOccurred())
+		stdout, err := kubectl("exec", "-n", ns, "ubuntu", "--", "cat", writePath)
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(strings.TrimSpace(string(stdout))).ShouldNot(BeEmpty())
+
+		By("deleting the Pod, then recreating it")
+		_, err = kubectl("delete", "--now=true", "-n", ns, "pod/ubuntu")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(podYaml, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("confirming that the file exists")
+		Eventually(func() error {
+			stdout, err = kubectl("exec", "-n", ns, "ubuntu", "--", "cat", writePath)
+			if err != nil {
+				return fmt.Errorf("failed to cat. err: %w", err)
+			}
+			if len(strings.TrimSpace(string(stdout))) == 0 {
+				return fmt.Errorf(writePath + " is empty")
+			}
+			return nil
+		}).Should(Succeed())
+
+		By("confirming that the lv correspond to LogicalVolume resource is registered in LVM")
+		var pvc corev1.PersistentVolumeClaim
+		err = getObjects(&pvc, "pvc", "-n", ns, "topo-pvc")
+		Expect(err).ShouldNot(HaveOccurred())
+		Eventually(func() error {
+			return checkLVIsRegisteredInLVM(pvc.Spec.VolumeName)
+		}).Should(Succeed())
+
+		By("confirming that the lv was successfully sized to the minimum size of the StorageClass")
+		Expect(pvc.Status.Capacity.Storage().Cmp(resource.MustParse(app.DefaultMinimumAllocationSizeXFS))).Should(Equal(0),
+			"expected: %s as minimum capacity, actual: %s", app.DefaultMinimumAllocationSizeXFS, pvc.Status.Capacity.Storage().String())
+
+		By("deleting the Pod and PVC")
+		_, err = kubectlWithInput(podYaml, "delete", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(claimYAML, "delete", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("confirming that the PV is deleted")
+		Eventually(func() error {
+			var pv corev1.PersistentVolume
+			err := getObjects(&pv, "pv", volName)
+			switch {
+			case err == ErrObjectNotFound:
+				return nil
+			case err != nil:
+				return fmt.Errorf("failed to get pv/%s. err: %w", volName, err)
+			default:
+				return fmt.Errorf("target pv exists %s", volName)
+			}
+		}).Should(Succeed())
+
+		By("confirming that the lv correspond to LogicalVolume is deleted")
+		Eventually(func() error {
+			return checkLVIsDeletedInLVM(volName)
+		}).Should(Succeed())
+	})
+
 	It("should use generic ephemeral volumes", func() {
 		By("deploying a Pod with a generic ephemeral volume")
 		_, err := kubectlWithInput(e2eGenericEphemeralVolumeYAML, "apply", "-n", ns, "-f", "-")
@@ -214,6 +310,92 @@ func testE2E() {
 
 		By("deploying ubuntu Pod with PVC to mount a block device")
 		podYAML := []byte(fmt.Sprintf(podVolumeDeviceTemplateYAML, deviceFile))
+
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 1024, "topolvm-provisioner"))
+
+		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(podYAML, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("confirming that a block device exists in ubuntu pod")
+		Eventually(func() error {
+			_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "test", "-b", deviceFile)
+			if err != nil {
+				podinfo, _ := kubectl("-n", ns, "describe", "pod", "ubuntu")
+				return fmt.Errorf("failed to test. ubuntu pod info output: %s; err: %w", podinfo, err)
+			}
+			return nil
+		}).Should(Succeed())
+
+		By("writing data to a block device")
+		// /etc/hostname contains "ubuntu"
+		_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "dd", "if=/etc/hostname", "of="+deviceFile)
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectl("exec", "-n", ns, "ubuntu", "--", "sync")
+		Expect(err).ShouldNot(HaveOccurred())
+		stdout, err := kubectl("exec", "-n", ns, "ubuntu", "--", "dd", "if="+deviceFile, "of=/dev/stdout", "bs=6", "count=1", "status=none")
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(string(stdout)).Should(Equal("ubuntu"))
+
+		By("deleting the Pod, then recreating it")
+		_, err = kubectl("delete", "--now=true", "-n", ns, "pod/ubuntu")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(podYAML, "apply", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("reading data from a block device")
+		Eventually(func() error {
+			stdout, err = kubectl("exec", "-n", ns, "ubuntu", "--", "dd", "if="+deviceFile, "of=/dev/stdout", "bs=6", "count=1", "status=none")
+			if err != nil {
+				return fmt.Errorf("failed to cat. err: %w", err)
+			}
+			if string(stdout) != "ubuntu" {
+				return fmt.Errorf("expected: ubuntu, actual: %s", string(stdout))
+			}
+			return nil
+		}).Should(Succeed())
+
+		By("confirming that the lv correspond to LogicalVolume resource is registered in LVM")
+		var pvc corev1.PersistentVolumeClaim
+		err = getObjects(&pvc, "pvc", "-n", ns, "topo-pvc")
+		Expect(err).ShouldNot(HaveOccurred())
+		Eventually(func() error {
+			return checkLVIsRegisteredInLVM(pvc.Spec.VolumeName)
+		}).Should(Succeed())
+
+		By("deleting the Pod and PVC")
+		_, err = kubectlWithInput(podYAML, "delete", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+		_, err = kubectlWithInput(claimYAML, "delete", "-n", ns, "-f", "-")
+		Expect(err).ShouldNot(HaveOccurred())
+
+		By("confirming that the PV is deleted")
+		Eventually(func() error {
+			var pv corev1.PersistentVolume
+			err := getObjects(&pv, "pv", volName)
+			switch {
+			case err == ErrObjectNotFound:
+				return nil
+			case err != nil:
+				return fmt.Errorf("failed to get pv/%s. err: %w", volName, err)
+			default:
+				return fmt.Errorf("target PV exists %s", volName)
+			}
+		}).Should(Succeed())
+
+		By("confirming that the lv correspond to LogicalVolume is deleted")
+		Eventually(func() error {
+			return checkLVIsDeletedInLVM(volName)
+		}).Should(Succeed())
+	})
+
+	It("should create a block device for Pod but changed by the minimum allocation default for block storage", func() {
+		deviceFile := "/dev/e2etest"
+
+		By("deploying ubuntu Pod with PVC to mount a block device")
+		podYAML := []byte(fmt.Sprintf(podVolumeDeviceTemplateYAML, deviceFile))
+
 		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 1, "topolvm-provisioner"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
@@ -267,6 +449,10 @@ func testE2E() {
 			return checkLVIsRegisteredInLVM(pvc.Spec.VolumeName)
 		}).Should(Succeed())
 
+		By("confirming that the lv was successfully sized to the minimum size of the StorageClass")
+		Expect(pvc.Status.Capacity.Storage().Cmp(resource.MustParse(app.DefaultMinimumAllocationSizeBlock))).Should(Equal(0),
+			"expected: %s as minimum capacity, actual: %s", app.DefaultMinimumAllocationSizeBlock, pvc.Status.Capacity.Storage().String())
+
 		By("deleting the Pod and PVC")
 		_, err = kubectlWithInput(podYAML, "delete", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
@@ -336,7 +522,7 @@ func testE2E() {
 			}).Should(Succeed())
 
 			By("creating pvc")
-			claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, fmt.Sprintf("topo-pvc-%d", i), "Filesystem", 1, "topolvm-provisioner-immediate"))
+			claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, fmt.Sprintf("topo-pvc-%d", i), "Filesystem", 1024, "topolvm-provisioner-immediate"))
 			_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 			Expect(err).ShouldNot(HaveOccurred())
 
@@ -368,7 +554,7 @@ func testE2E() {
 
 	It("should scheduled onto the correct node where PV exists (volumeBindingMode == Immediate)", func() {
 		By("creating pvc")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1, "topolvm-provisioner-immediate"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1024, "topolvm-provisioner-immediate"))
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -549,7 +735,7 @@ func testE2E() {
 
 	It("should resize filesystem", func() {
 		By("deploying Pod with PVC")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1, "topolvm-provisioner"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1024, "topolvm-provisioner"))
 		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu", "topo-pvc"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
@@ -563,7 +749,7 @@ func testE2E() {
 		}).Should(Succeed())
 
 		By("resizing PVC online")
-		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 2, "topolvm-provisioner"))
+		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 2*1024, "topolvm-provisioner"))
 		_, err = kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -590,7 +776,7 @@ func testE2E() {
 		Expect(err).ShouldNot(HaveOccurred())
 
 		By("resizing PVC offline")
-		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 3, "topolvm-provisioner"))
+		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 3*1024, "topolvm-provisioner"))
 		_, err = kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -620,7 +806,7 @@ func testE2E() {
 		Expect(err).ShouldNot(HaveOccurred())
 
 		By("resizing PVC")
-		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 4, "topolvm-provisioner"))
+		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 4*1024, "topolvm-provisioner"))
 		_, err = kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -650,7 +836,7 @@ func testE2E() {
 		Expect(err).To(BeEquivalentTo(ErrObjectNotFound))
 
 		By("resizing PVC over vg capacity")
-		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 100, "topolvm-provisioner"))
+		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 100*1024, "topolvm-provisioner"))
 		_, err = kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -679,7 +865,7 @@ func testE2E() {
 		By("deploying Pod with PVC")
 		deviceFile := "/dev/e2etest"
 		podYAML := []byte(fmt.Sprintf(podVolumeDeviceTemplateYAML, deviceFile))
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 1, "topolvm-provisioner"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 1024, "topolvm-provisioner"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
@@ -696,7 +882,7 @@ func testE2E() {
 		}).Should(Succeed())
 
 		By("resizing PVC")
-		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 2, "topolvm-provisioner"))
+		claimYAML = []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Block", 2048, "topolvm-provisioner"))
 		_, err = kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
 		Expect(err).ShouldNot(HaveOccurred())
 
@@ -726,7 +912,7 @@ func testE2E() {
 
 	It("should delete a pod when the pvc is deleted", func() {
 		By("deploying a pod and PVC")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1, "topolvm-provisioner"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1024, "topolvm-provisioner"))
 		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu", "topo-pvc"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
diff --git a/test/e2e/lvcreate_options_test.go b/test/e2e/lvcreate_options_test.go
index 3a5994cc..bd24b8a8 100644
--- a/test/e2e/lvcreate_options_test.go
+++ b/test/e2e/lvcreate_options_test.go
@@ -34,7 +34,7 @@ func testLVCreateOptions() {
 
 	It("should use lvcreate-options when creating LV", func() {
 		By("creating Pod with PVC using raid device-class")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1, "topolvm-provisioner-create-option-raid1"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc", "Filesystem", 1024, "topolvm-provisioner-create-option-raid1"))
 		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu", "topo-pvc"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
@@ -71,7 +71,7 @@ func testLVCreateOptions() {
 
 	It("should use lvcreate-option-classes when creating LV", func() {
 		By("creating Pod with PVC using raid1 device-class")
-		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc-raid1", "Filesystem", 1, "topolvm-provisioner-option-class-raid1"))
+		claimYAML := []byte(fmt.Sprintf(pvcTemplateYAML, "topo-pvc-raid1", "Filesystem", 1024, "topolvm-provisioner-option-class-raid1"))
 		podYaml := []byte(fmt.Sprintf(podVolumeMountTemplateYAML, "ubuntu2", "topo-pvc-raid1"))
 
 		_, err := kubectlWithInput(claimYAML, "apply", "-n", ns, "-f", "-")
diff --git a/test/e2e/testdata/e2e/pvc-template.yaml b/test/e2e/testdata/e2e/pvc-template.yaml
index 7a1bd5b1..f606ddf7 100644
--- a/test/e2e/testdata/e2e/pvc-template.yaml
+++ b/test/e2e/testdata/e2e/pvc-template.yaml
@@ -8,5 +8,5 @@ spec:
   - ReadWriteOnce
   resources:
     requests:
-      storage: %dGi
+      storage: %dMi
   storageClassName: %s