diff --git a/docs/parameters.md b/docs/parameters.md index 5790d6c5d9..77c7dbf46b 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -11,6 +11,7 @@ The AWS EBS CSI Driver supports [tagging](tagging.md) through `StorageClass.para | "type" | io1, io2, gp2, gp3, sc1, st1, standard, sbp1, sbg1 | gp3* | EBS volume type. | | "iopsPerGB" | | | I/O operations per second per GiB. Can be specified for IO1, IO2, and GP3 volumes. | | "allowAutoIOPSPerGBIncrease" | true, false | false | When `"true"`, the CSI driver increases IOPS for a volume when `iopsPerGB * ` is too low to fit into IOPS range supported by AWS. This allows dynamic provisioning to always succeed, even when user specifies too small PVC capacity or `iopsPerGB` value. On the other hand, it may introduce additional costs, as such volumes have higher IOPS than requested in `iopsPerGB`. | +| "allowAutoVolumeSizeIncrease" | true, false | false | When `"true"`, the CSI driver adjust volume size when it is too low to fit into Volume size range supported by AWS. This allows dynamic provisioning to always succeed, even when user specifies too small PVC size. On the other hand, it may introduce additional costs, as such volumes have higher size than requested. | | "iops" | | | I/O operations per second. Can be specified for IO1, IO2, and GP3 volumes. | | "throughput" | | 125 | Throughput in MiB/s. Only effective when gp3 volume type is specified. If empty, it will set to 125MiB/s as documented [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html). | | "encrypted" | true, false | false | Whether the volume should be encrypted or not. Valid values are "true" or "false". | diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index fca0bab157..95cdd562f8 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -69,13 +69,23 @@ const ( io1MinTotalIOPS = 100 io1MaxTotalIOPS = 64000 io1MaxIOPSPerGB = 50 + io1MinSize = 4 + io1MaxSize = 16 * 1024 // 16 TiB io2MinTotalIOPS = 100 io2MaxTotalIOPS = 64000 io2BlockExpressMaxTotalIOPS = 256000 io2MaxIOPSPerGB = 500 + io2MinSize = 4 + io2MaxSize = 64 * 1024 // 64 TiB gp3MaxTotalIOPS = 16000 gp3MinTotalIOPS = 3000 gp3MaxIOPSPerGB = 500 + gpMinSize = 1 + gpMaxSize = 16 * 1024 // 16 TiB + hddMinSize = 125 + hddMaxSize = 16 * 1024 // 16 TiB + standardMinSize = 1 + standardMaxSize = 1024 ) var ( @@ -204,18 +214,19 @@ type Disk struct { // DiskOptions represents parameters to create an EBS volume. type DiskOptions struct { - CapacityBytes int64 - Tags map[string]string - VolumeType string - IOPSPerGB int32 - AllowIOPSPerGBIncrease bool - IOPS int32 - Throughput int32 - AvailabilityZone string - OutpostArn string - Encrypted bool - BlockExpress bool - MultiAttachEnabled bool + CapacityBytes int64 + Tags map[string]string + VolumeType string + IOPSPerGB int32 + AllowIOPSPerGBIncrease bool + AllowVolumeSizeIncrease bool + IOPS int32 + Throughput int32 + AvailabilityZone string + OutpostArn string + Encrypted bool + BlockExpress bool + MultiAttachEnabled bool // KmsKeyID represents a fully qualified resource name to the key to use for encryption. // example: arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef KmsKeyID string @@ -533,6 +544,8 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions * minIops int32 maxIopsPerGb int32 requestedIops int32 + minVolumeSize int32 + maxVolumeSize int32 ) capacityGiB := util.BytesToGiB(diskOptions.CapacityBytes) @@ -548,11 +561,22 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions * } switch createType { - case VolumeTypeGP2, VolumeTypeSC1, VolumeTypeST1, VolumeTypeSBG1, VolumeTypeSBP1, VolumeTypeStandard: + case VolumeTypeSBG1, VolumeTypeSBP1: + case VolumeTypeSC1, VolumeTypeST1: + minVolumeSize = hddMinSize + maxVolumeSize = hddMaxSize + case VolumeTypeStandard: + minVolumeSize = standardMinSize + maxVolumeSize = standardMaxSize + case VolumeTypeGP2: + minVolumeSize = gpMinSize + maxVolumeSize = gpMaxSize case VolumeTypeIO1: maxIops = io1MaxTotalIOPS minIops = io1MinTotalIOPS maxIopsPerGb = io1MaxIOPSPerGB + minVolumeSize = io1MinSize + maxVolumeSize = io1MaxSize case VolumeTypeIO2: if diskOptions.BlockExpress { maxIops = io2BlockExpressMaxTotalIOPS @@ -561,11 +585,15 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions * } minIops = io2MinTotalIOPS maxIopsPerGb = io2MaxIOPSPerGB + minVolumeSize = io2MinSize + maxVolumeSize = io2MaxSize case VolumeTypeGP3: maxIops = gp3MaxTotalIOPS minIops = gp3MinTotalIOPS maxIopsPerGb = gp3MaxIOPSPerGB throughput = diskOptions.Throughput + minVolumeSize = gpMinSize + maxVolumeSize = gpMaxSize default: return nil, fmt.Errorf("invalid AWS VolumeType %q", diskOptions.VolumeType) } @@ -574,6 +602,10 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions * return nil, errors.New("CreateDisk: multi-attach is only supported for io2 volumes") } + if maxVolumeSize > 0 { + capacityGiB = capVolumeSize(createType, capacityGiB, minVolumeSize, maxVolumeSize, diskOptions.AllowVolumeSizeIncrease) + } + if maxIops > 0 { if diskOptions.IOPS > 0 { requestedIops = diskOptions.IOPS @@ -1946,3 +1978,25 @@ func capIOPS(volumeType string, requestedCapacityGiB int32, requestedIops int32, } return iops } + +// Calculate actual Volume Size for a volume and cap it at supported AWS limits. +func capVolumeSize(volumeType string, requestedCapacityGiB int32, minSize, maxSize int32, allowIncrease bool) int32 { + // If requestedCapacityGiB is zero the user did not request a specific amount, and the default will be used instead + if requestedCapacityGiB == 0 { + return 0 + } + + size := requestedCapacityGiB + + if size < minSize { + if allowIncrease { + size = minSize + klog.V(5).InfoS("[Debug] Increased Volume Size to the min supported limit", "volumeType", volumeType, "requestedCapacityGiB", requestedCapacityGiB, "limit", minSize) + } + } + if size > maxSize { + size = maxSize + klog.V(5).InfoS("[Debug] Capped Volume Size, volume at the max supported limit", "volumeType", volumeType, "requestedCapacityGiB", requestedCapacityGiB, "limit", maxSize) + } + return size +} diff --git a/pkg/cloud/cloud_test.go b/pkg/cloud/cloud_test.go index 02e111420f..87fa8a8487 100644 --- a/pkg/cloud/cloud_test.go +++ b/pkg/cloud/cloud_test.go @@ -1263,6 +1263,208 @@ func TestCreateDisk(t *testing.T) { }, expErr: nil, }, + { + name: "success: io1 with too low capacity and AllowVolumeSizeIncrease", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.GiBToBytes(1), + Tags: map[string]string{VolumeNameTagKey: "vol-test", AwsEbsDriverTagKey: "true"}, + VolumeType: VolumeTypeIO1, + AllowVolumeSizeIncrease: true, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: 1, + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(4), + }, + expErr: nil, + }, + { + name: "success: io1 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(17), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeIO1, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(17), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(16)), + }, + expErr: nil, + }, + { + name: "success: io2 with too low capacity and AllowVolumeSizeIncrease", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.GiBToBytes(1), + Tags: map[string]string{VolumeNameTagKey: "vol-test", AwsEbsDriverTagKey: "true"}, + VolumeType: VolumeTypeIO2, + AllowVolumeSizeIncrease: true, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: 1, + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(4), + }, + expErr: nil, + }, + { + name: "success: io2 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(65), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeIO2, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(65), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(64)), + }, + expErr: nil, + }, + { + name: "success: gp2 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(17), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeGP2, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(17), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(16)), + }, + expErr: nil, + }, + { + name: "success: gp3 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(17), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeGP3, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(17), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(16)), + }, + expErr: nil, + }, + { + name: "success: st1 with too low capacity and AllowVolumeSizeIncrease", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.GiBToBytes(100), + Tags: map[string]string{VolumeNameTagKey: "vol-test", AwsEbsDriverTagKey: "true"}, + VolumeType: VolumeTypeST1, + AllowVolumeSizeIncrease: true, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: 100, + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(125), + }, + expErr: nil, + }, + { + name: "success: st1 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(17), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeST1, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(17), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(16)), + }, + expErr: nil, + }, + { + name: "success: sc1 with too low capacity and AllowVolumeSizeIncrease", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.GiBToBytes(100), + Tags: map[string]string{VolumeNameTagKey: "vol-test", AwsEbsDriverTagKey: "true"}, + VolumeType: VolumeTypeSC1, + AllowVolumeSizeIncrease: true, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: 100, + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(125), + }, + expErr: nil, + }, + { + name: "success: sc1 with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(17), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeSC1, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(17), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(16)), + }, + expErr: nil, + }, + { + name: "success: standard with too high capacity", + volumeName: "vol-test-name", + diskOptions: &DiskOptions{ + CapacityBytes: util.TiBToBytes(2), + Tags: map[string]string{VolumeNameTagKey: "vol-test"}, + VolumeType: VolumeTypeStandard, + }, + expDisk: &Disk{ + VolumeID: "vol-test", + CapacityGiB: util.TiBToGiB(2), + AvailabilityZone: defaultZone, + }, + expCreateVolumeInput: &ec2.CreateVolumeInput{ + Size: aws.Int32(util.TiBToGiB(1)), + }, + expErr: nil, + }, { name: "success: create volume when zone is snow and add tags", volumeName: "vol-test-name", diff --git a/pkg/driver/constants.go b/pkg/driver/constants.go index 71dfe4f157..f2186295f1 100644 --- a/pkg/driver/constants.go +++ b/pkg/driver/constants.go @@ -43,6 +43,9 @@ const ( // AllowAutoIOPSPerGBIncreaseKey represents key for allowing automatic increase of IOPS. AllowAutoIOPSPerGBIncreaseKey = "allowautoiopspergbincrease" + // AllowAutoVolumeSizeIncreaseKey represents key for allowing automatic increase of Volume size. + AllowAutoVolumeSizeIncreaseKey = "allowautovolumesizeincrease" + // Iops represents key for IOPS for volume. IopsKey = "iops" diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 352cc278c2..78e3e7a24d 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -106,16 +106,17 @@ func (d *ControllerService) CreateVolume(ctx context.Context, req *csi.CreateVol defer d.inFlight.Delete(volName) var ( - volumeType string - iopsPerGB int32 - allowIOPSPerGBIncrease bool - iops int32 - throughput int32 - isEncrypted bool - blockExpress bool - kmsKeyID string - scTags []string - volumeTags = map[string]string{ + volumeType string + iopsPerGB int32 + allowIOPSPerGBIncrease bool + allowVolumeSizeIncrease bool + iops int32 + throughput int32 + isEncrypted bool + blockExpress bool + kmsKeyID string + scTags []string + volumeTags = map[string]string{ cloud.VolumeNameTagKey: volName, cloud.AwsEbsDriverTagKey: isManagedByDriver, } @@ -143,6 +144,8 @@ func (d *ControllerService) CreateVolume(ctx context.Context, req *csi.CreateVol iopsPerGB = int32(parseIopsPerGBKey) case AllowAutoIOPSPerGBIncreaseKey: allowIOPSPerGBIncrease = isTrue(value) + case AllowAutoVolumeSizeIncreaseKey: + allowVolumeSizeIncrease = isTrue(value) case IopsKey: parseIopsKey, parseIopsKeyErr := strconv.ParseInt(value, 10, 32) if parseIopsKeyErr != nil { @@ -312,20 +315,21 @@ func (d *ControllerService) CreateVolume(ctx context.Context, req *csi.CreateVol } opts := &cloud.DiskOptions{ - CapacityBytes: volSizeBytes, - Tags: volumeTags, - VolumeType: volumeType, - IOPSPerGB: iopsPerGB, - AllowIOPSPerGBIncrease: allowIOPSPerGBIncrease, - IOPS: iops, - Throughput: throughput, - AvailabilityZone: zone, - OutpostArn: outpostArn, - Encrypted: isEncrypted, - BlockExpress: blockExpress, - KmsKeyID: kmsKeyID, - SnapshotID: snapshotID, - MultiAttachEnabled: multiAttach, + CapacityBytes: volSizeBytes, + Tags: volumeTags, + VolumeType: volumeType, + IOPSPerGB: iopsPerGB, + AllowIOPSPerGBIncrease: allowIOPSPerGBIncrease, + AllowVolumeSizeIncrease: allowVolumeSizeIncrease, + IOPS: iops, + Throughput: throughput, + AvailabilityZone: zone, + OutpostArn: outpostArn, + Encrypted: isEncrypted, + BlockExpress: blockExpress, + KmsKeyID: kmsKeyID, + SnapshotID: snapshotID, + MultiAttachEnabled: multiAttach, } disk, err := d.cloud.CreateDisk(ctx, volName, opts) diff --git a/pkg/util/util.go b/pkg/util/util.go index 346dde5d5a..246753907d 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -32,6 +32,7 @@ import ( const ( GiB = int64(1024 * 1024 * 1024) + TiB = int64(1024 * 1024 * 1024 * 1024) DefaultBlockSize = 4096 ) @@ -73,6 +74,16 @@ func GiBToBytes(volumeSizeGiB int32) int64 { return int64(volumeSizeGiB) * GiB } +// TiBToBytes converts TiB to Bytes. +func TiBToBytes(volumeSizeTiB int32) int64 { + return int64(volumeSizeTiB) * TiB +} + +// TiBToGiB converts TiB to GiB. +func TiBToGiB(volumeSizeTiB int32) int32 { + return volumeSizeTiB * 1024 +} + func ParseEndpoint(endpoint string, hostprocess bool) (string, string, error) { if runtime.GOOS == "windows" && hostprocess { parts := strings.SplitN(endpoint, "://", 2)