Repository: keikoproj/instance-manager (go)

Issue:
Allow IOPS config option for io1 volumeType 
**Is this a BUG REPORT or FEATURE REQUEST?**:
FEATURE
**What happened**:
Allow IOPS config option for io1 volumeType 

https://github.com/keikoproj/instance-manager/blob/5843c10e7b365f7206542e08efd754413d51799c/api/v1alpha1/instancegroup_types.go#L193-L196

More Details about io1
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html


**What you expected to happen**:

**How to reproduce it (as minimally and precisely as possible)**:

**Anything else we need to know?**:

**Environment**:

- Kubernetes version:

```bash
kubectl version -o yaml
```

**Other debugging information (if applicable)**:

- InstanceGroup status:

```bash
kubectl describe instancegroup <ig-name>
```

- controller logs:

```bash
kubectl logs <instance-manager pod>
```


Pull request description:
Additional volume options
Fixes #155 

Adds additional volume options to NodeVolumes

Patch:
diff --git a/api/v1alpha1/instancegroup_types.go b/api/v1alpha1/instancegroup_types.go
index bf03559..69c4d5e 100644
--- a/api/v1alpha1/instancegroup_types.go
+++ b/api/v1alpha1/instancegroup_types.go
@@ -187,14 +187,18 @@ type EKSConfiguration struct {
 
 type UserDataStage struct {
 	Name  string `json:"name,omitempty"`
-	Stage string `json:"stage,omitempty"`
-	Data  string `json:"data,omitempty"`
+	Stage string `json:"stage"`
+	Data  string `json:"data"`
 }
 
 type NodeVolume struct {
-	Name string `json:"name,omitempty"`
-	Type string `json:"type,omitempty"`
-	Size int64  `json:"size,omitempty"`
+	Name                string `json:"name"`
+	Type                string `json:"type"`
+	Size                int64  `json:"size"`
+	Iops                int64  `json:"iops,omitempty"`
+	DeleteOnTermination *bool  `json:"deleteOnTermination,omitempty"`
+	Encrypted           *bool  `json:"encrypted,omitempty"`
+	SnapshotID          string `json:"snapshotId,omitempty"`
 }
 type EKSFargateSpec struct {
 	ClusterName         string                `json:"clusterName"`
@@ -313,6 +317,21 @@ func (c *EKSConfiguration) Validate() error {
 	if common.StringEmpty(c.KeyPairName) {
 		return errors.Errorf("validation failed, 'keyPair' is a required parameter")
 	}
+
+	for _, v := range c.Volumes {
+		if !common.ContainsEqualFold(awsprovider.AllowedVolumeTypes, v.Type) {
+			return errors.Errorf("validation failed, volume type '%v' is unsuppoeted", v.Type)
+		}
+		if v.SnapshotID != "" {
+			if v.Size > 0 {
+				return errors.Errorf("validation failed, 'volume.snapshotId' and 'volume.size' are mutually exclusive")
+			}
+		}
+		if v.Iops != 0 && v.Iops < 100 {
+			return errors.Errorf("validation failed, volume IOPS must be min 100")
+		}
+	}
+
 	if len(c.Volumes) == 0 {
 		c.Volumes = []NodeVolume{
 			{
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 7cd7008..dd009eb 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -76,7 +76,9 @@ func (in *EKSConfiguration) DeepCopyInto(out *EKSConfiguration) {
 	if in.Volumes != nil {
 		in, out := &in.Volumes, &out.Volumes
 		*out = make([]NodeVolume, len(*in))
-		copy(*out, *in)
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
 	}
 	if in.Subnets != nil {
 		in, out := &in.Subnets, &out.Subnets
@@ -417,6 +419,16 @@ func (in *InstanceGroupStatus) DeepCopy() *InstanceGroupStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NodeVolume) DeepCopyInto(out *NodeVolume) {
 	*out = *in
+	if in.DeleteOnTermination != nil {
+		in, out := &in.DeleteOnTermination, &out.DeleteOnTermination
+		*out = new(bool)
+		**out = **in
+	}
+	if in.Encrypted != nil {
+		in, out := &in.Encrypted, &out.Encrypted
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeVolume.
diff --git a/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml b/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
index 4fa6750..f44888a 100644
--- a/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
+++ b/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
@@ -158,18 +158,34 @@ spec:
                             type: string
                           stage:
                             type: string
+                        required:
+                        - data
+                        - stage
                         type: object
                       type: array
                     volumes:
                       items:
                         properties:
+                          deleteOnTermination:
+                            type: boolean
+                          encrypted:
+                            type: boolean
+                          iops:
+                            format: int64
+                            type: integer
                           name:
                             type: string
                           size:
                             format: int64
                             type: integer
+                          snapshotId:
+                            type: string
                           type:
                             type: string
+                        required:
+                        - name
+                        - size
+                        - type
                         type: object
                       type: array
                   type: object
diff --git a/controllers/providers/aws/aws.go b/controllers/providers/aws/aws.go
index acdb281..e241fbf 100644
--- a/controllers/providers/aws/aws.go
+++ b/controllers/providers/aws/aws.go
@@ -101,6 +101,8 @@ var (
 		"GroupTotalInstances",
 		"GroupTotalCapacity",
 	}
+
+	AllowedVolumeTypes = []string{"gp2", "io1", "sc1", "st1"}
 )
 
 const (
@@ -156,15 +158,32 @@ func (w *AwsWorker) InstanceProfileExist(name string) (*iam.InstanceProfile, boo
 	return out.InstanceProfile, true
 }
 
-func (w *AwsWorker) GetBasicBlockDevice(name, volType string, volSize int64) *autoscaling.BlockDeviceMapping {
-	return &autoscaling.BlockDeviceMapping{
+func (w *AwsWorker) GetBasicBlockDevice(name, volType, snapshot string, volSize, iops int64, delete, encrypt *bool) *autoscaling.BlockDeviceMapping {
+	device := &autoscaling.BlockDeviceMapping{
 		DeviceName: aws.String(name),
 		Ebs: &autoscaling.Ebs{
-			VolumeSize:          aws.Int64(volSize),
-			VolumeType:          aws.String(volType),
-			DeleteOnTermination: aws.Bool(true),
+			VolumeType: aws.String(volType),
 		},
 	}
+	if delete != nil {
+		device.Ebs.DeleteOnTermination = delete
+	} else {
+		device.Ebs.DeleteOnTermination = aws.Bool(true)
+	}
+	if encrypt != nil {
+		device.Ebs.Encrypted = encrypt
+	}
+	if iops != 0 {
+		device.Ebs.Iops = aws.Int64(iops)
+	}
+	if volSize != 0 {
+		device.Ebs.VolumeSize = aws.Int64(volSize)
+	}
+	if !common.StringEmpty(snapshot) {
+		device.Ebs.SnapshotId = aws.String(snapshot)
+	}
+
+	return device
 }
 
 func (w *AwsWorker) CreateLaunchConfig(input *autoscaling.CreateLaunchConfigurationInput) error {
diff --git a/controllers/provisioners/eks/helpers.go b/controllers/provisioners/eks/helpers.go
index 34a7150..140c384 100644
--- a/controllers/provisioners/eks/helpers.go
+++ b/controllers/provisioners/eks/helpers.go
@@ -237,7 +237,7 @@ func (ctx *EksInstanceGroupContext) GetBlockDeviceList() []*autoscaling.BlockDev
 
 	customVolumes := configuration.GetVolumes()
 	for _, v := range customVolumes {
-		devices = append(devices, ctx.AwsWorker.GetBasicBlockDevice(v.Name, v.Type, v.Size))
+		devices = append(devices, ctx.AwsWorker.GetBasicBlockDevice(v.Name, v.Type, v.SnapshotID, v.Size, v.Iops, v.DeleteOnTermination, v.Encrypted))
 	}
 
 	return devices
diff --git a/docs/EKS.md b/docs/EKS.md
index 4ff20a2..01a2e73 100644
--- a/docs/EKS.md
+++ b/docs/EKS.md
@@ -98,8 +98,8 @@ spec:
     configuration:
       userData:
       - name: <string> : name of the stage
-        stage: <string> : represents the stage of the script, allowed values are PreBootstrap, PostBootstrap
-        data: <string> : represents the script payload to inject
+        stage: <string> : represents the stage of the script, allowed values are PreBootstrap, PostBootstrap (required)
+        data: <string> : represents the script payload to inject (required)
 ```
 
 ### NodeVolume
@@ -112,9 +112,13 @@ spec:
   eks:
     configuration:
       volumes:
-      - name: <string> : represents the device name, e.g. /dev/xvda
-        type: <string> : represents the type of volume, must be one of supported types "standard", "io1", "gp2", "st1", "sc1".
-        size: <int64> : represents a volume size in gigabytes
+      - name: <string> : represents the device name, e.g. /dev/xvda (required)
+        type: <string> : represents the type of volume, must be one of supported types "standard", "io1", "gp2", "st1", "sc1" (required)
+        size: <int64> : represents a volume size in gigabytes, cannot be used with snapshotId
+        snapshotId : <string> : represents a snapshot ID to use, cannot be used with size
+        iops: <int64> : represents number of IOPS to provision volume with (min 100)
+        deleteOnTermination : <bool> : delete the EBS volume when the instance is terminated (defaults to true)
+        encrypted: <bool> : encrypt the EBS volume with a KMS key
 ```
 
 ### Taint


Test patch:
diff --git a/controllers/provisioners/eks/update_test.go b/controllers/provisioners/eks/update_test.go
index 7a0cddd..45e2882 100644
--- a/controllers/provisioners/eks/update_test.go
+++ b/controllers/provisioners/eks/update_test.go
@@ -325,7 +325,7 @@ func TestLaunchConfigurationDrifted(t *testing.T) {
 	keyDrift.KeyName = aws.String("some-key")
 	usrDrift.UserData = aws.String("some-userdata")
 	devDrift.BlockDeviceMappings = []*autoscaling.BlockDeviceMapping{
-		w.GetBasicBlockDevice("some-device", "some-type", 0),
+		w.GetBasicBlockDevice("some-device", "some-type", "", 32, 0, nil, nil),
 	}
 
 	tests := []struct {