Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple block device mappings #299

Merged
merged 1 commit into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 48 additions & 26 deletions pkg/actuators/machine/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package machine

import (
"encoding/base64"
"errors"
"fmt"
"sort"
"strconv"
Expand Down Expand Up @@ -181,9 +182,11 @@ func getAMI(AMI awsproviderv1.AWSResourceReference, client awsclient.Client) (*s
return nil, fmt.Errorf("AMI ID or AMI filters need to be specified")
}

func getBlockDeviceMappings(blockDeviceMappings []awsproviderv1.BlockDeviceMappingSpec, AMI string, client awsclient.Client) ([]*ec2.BlockDeviceMapping, error) {
if len(blockDeviceMappings) == 0 || blockDeviceMappings[0].EBS == nil {
return []*ec2.BlockDeviceMapping{}, nil
func getBlockDeviceMappings(blockDeviceMappingSpecs []awsproviderv1.BlockDeviceMappingSpec, AMI string, client awsclient.Client) ([]*ec2.BlockDeviceMapping, error) {
blockDeviceMappings := make([]*ec2.BlockDeviceMapping, 0)

if len(blockDeviceMappingSpecs) == 0 {
return blockDeviceMappings, nil
}

// Get image object to get the RootDeviceName
Expand All @@ -199,33 +202,52 @@ func getBlockDeviceMappings(blockDeviceMappings []awsproviderv1.BlockDeviceMappi
klog.Errorf("No image for given AMI was found")
return nil, fmt.Errorf("no image for given AMI not found")
}
deviceName := describeAMIResult.Images[0].RootDeviceName

// Only support one blockDeviceMapping
volumeSize := blockDeviceMappings[0].EBS.VolumeSize
volumeType := blockDeviceMappings[0].EBS.VolumeType
blockDeviceMapping := ec2.BlockDeviceMapping{
DeviceName: deviceName,
Ebs: &ec2.EbsBlockDevice{
VolumeSize: volumeSize,
VolumeType: volumeType,
Encrypted: blockDeviceMappings[0].EBS.Encrypted,
},
}

if aws.StringValue(blockDeviceMappings[0].EBS.KMSKey.ID) != "" {
klog.V(3).Infof("Using KMS key ID %q for encrypting EBS volume", *blockDeviceMappings[0].EBS.KMSKey.ID)
blockDeviceMapping.Ebs.KmsKeyId = blockDeviceMappings[0].EBS.KMSKey.ID
} else if aws.StringValue(blockDeviceMappings[0].EBS.KMSKey.ARN) != "" {
klog.V(3).Info("Using KMS key ARN for encrypting EBS volume") // ARN usually have account ids, therefore are sensitive data so shouldn't log the value
blockDeviceMapping.Ebs.KmsKeyId = blockDeviceMappings[0].EBS.KMSKey.ARN
}
rootDeviceFound := false
for _, blockDeviceMappingSpec := range blockDeviceMappingSpecs {
if blockDeviceMappingSpec.EBS == nil {
continue
}

deviceName := blockDeviceMappingSpec.DeviceName
volumeSize := blockDeviceMappingSpec.EBS.VolumeSize
volumeType := blockDeviceMappingSpec.EBS.VolumeType
deleteOnTermination := true

if blockDeviceMappingSpec.DeviceName == nil {
if rootDeviceFound {
return nil, errors.New("non root device must have name")
}
rootDeviceFound = true
deviceName = describeAMIResult.Images[0].RootDeviceName
}

blockDeviceMapping := ec2.BlockDeviceMapping{
DeviceName: deviceName,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to attempt to give all devices the same name.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's expected that DeviceName should always be set apart from on the root device, though I don't think there's any validation on that at all since these aren't CRDs and we don't have schemas or webhooks for them.

Perhaps we can do validation here and return an error if more than one of the block device mappings doesn't have a name set?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoelSpeed I thought of this but wasn't sure that's a right thing to do in that case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we don't have any webhooks set up and we use a rawextension, I'm not sure what else we can do, I may well be missing something though, there may be a better way

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need webhooks. We can just fail the machine's reconcile and quit attempting. I consider this an advanced usecase, so the user better follow the docs and set it up correctly. This will probably also require investigation from the user to determine what a valid block device name is for their instance type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this, we should make sure the PR that is adding webhooks should validate this, or we should follow up to update that (cc @enxebre)

Copy link
Member

@enxebre enxebre Jun 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Captured this here -> https://github.com/openshift/machine-api-operator/pull/601/files#diff-98bc71b7347ada0763f9e57ae6c9d235R320
Regardless this should error and return and invalid config which the machine controller can use to fail. This seems to be case already.

Ebs: &ec2.EbsBlockDevice{
VolumeSize: volumeSize,
VolumeType: volumeType,
Encrypted: blockDeviceMappingSpec.EBS.Encrypted,
DeleteOnTermination: &deleteOnTermination,
},
}

if *volumeType == ec2.VolumeTypeIo1 {
blockDeviceMapping.Ebs.Iops = blockDeviceMappingSpec.EBS.Iops
}

if aws.StringValue(blockDeviceMappingSpec.EBS.KMSKey.ID) != "" {
klog.V(3).Infof("Using KMS key ID %q for encrypting EBS volume", *blockDeviceMappingSpec.EBS.KMSKey.ID)
blockDeviceMapping.Ebs.KmsKeyId = blockDeviceMappingSpec.EBS.KMSKey.ID
} else if aws.StringValue(blockDeviceMappingSpec.EBS.KMSKey.ARN) != "" {
klog.V(3).Info("Using KMS key ARN for encrypting EBS volume") // ARN usually have account ids, therefore are sensitive data so shouldn't log the value
blockDeviceMapping.Ebs.KmsKeyId = blockDeviceMappingSpec.EBS.KMSKey.ARN
}

if *volumeType == "io1" {
blockDeviceMapping.Ebs.Iops = blockDeviceMappings[0].EBS.Iops
blockDeviceMappings = append(blockDeviceMappings, &blockDeviceMapping)
}

return []*ec2.BlockDeviceMapping{&blockDeviceMapping}, nil
return blockDeviceMappings, nil
}

func launchInstance(machine *machinev1.Machine, machineProviderConfig *awsproviderv1.AWSMachineProviderConfig, userData []byte, client awsclient.Client) (*ec2.Instance, error) {
Expand Down
139 changes: 111 additions & 28 deletions pkg/actuators/machine/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ func TestBuildEC2Filters(t *testing.T) {
func TestGetBlockDeviceMappings(t *testing.T) {
rootDeviceName := "/dev/sda1"
volumeSize := int64(16384)
deviceName2 := "/dev/sda2"
volumeSize2 := int64(16385)
deleteOnTermination := true
volumeType := "ssd"

mockCtrl := gomock.NewController(t)
mockAWSClient := mockaws.NewMockClient(mockCtrl)
mockAWSClient.EXPECT().DescribeImages(gomock.Any()).Return(&ec2.DescribeImagesOutput{
Expand All @@ -106,50 +110,129 @@ func TestGetBlockDeviceMappings(t *testing.T) {
},
}, nil).AnyTimes()

oneBlockDevice := []awsproviderv1.BlockDeviceMappingSpec{
{
DeviceName: &rootDeviceName,
EBS: &awsproviderv1.EBSBlockDeviceSpec{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
},
NoDevice: nil,
VirtualName: nil,
},
}

oneExpectedBlockDevice := []*ec2.BlockDeviceMapping{
{
DeviceName: &rootDeviceName,
Ebs: &ec2.EbsBlockDevice{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
DeleteOnTermination: &deleteOnTermination,
},
NoDevice: nil,
VirtualName: nil,
},
}

blockDevices := []awsproviderv1.BlockDeviceMappingSpec{
{
DeviceName: &rootDeviceName,
EBS: &awsproviderv1.EBSBlockDeviceSpec{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
},
NoDevice: nil,
VirtualName: nil,
},
{
DeviceName: &deviceName2,
EBS: &awsproviderv1.EBSBlockDeviceSpec{
VolumeSize: &volumeSize2,
VolumeType: &volumeType,
},
NoDevice: nil,
VirtualName: nil,
},
}

twoExpectedDevices := []*ec2.BlockDeviceMapping{
{
DeviceName: &rootDeviceName,
Ebs: &ec2.EbsBlockDevice{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
DeleteOnTermination: &deleteOnTermination,
},
NoDevice: nil,
VirtualName: nil,
},
{
DeviceName: &deviceName2,
Ebs: &ec2.EbsBlockDevice{
VolumeSize: &volumeSize2,
VolumeType: &volumeType,
DeleteOnTermination: &deleteOnTermination,
},
NoDevice: nil,
VirtualName: nil,
},
}

blockDevicesOneEmptyName := make([]awsproviderv1.BlockDeviceMappingSpec, len(blockDevices))
copy(blockDevicesOneEmptyName, blockDevices)
blockDevicesOneEmptyName[0].DeviceName = nil

blockDevicesTwoEmptyNames := make([]awsproviderv1.BlockDeviceMappingSpec, len(blockDevicesOneEmptyName))
copy(blockDevicesTwoEmptyNames, blockDevicesOneEmptyName)
blockDevicesTwoEmptyNames[1].DeviceName = nil

testCases := []struct {
description string
blockDevices []awsproviderv1.BlockDeviceMappingSpec
expected []*ec2.BlockDeviceMapping
expectedErr bool
}{
{
description: "When it gets an empty blockDevices list",
blockDevices: []awsproviderv1.BlockDeviceMappingSpec{},
expected: []*ec2.BlockDeviceMapping{},
},
{
description: "When it gets one blockDevice",
blockDevices: []awsproviderv1.BlockDeviceMappingSpec{
{
DeviceName: &rootDeviceName,
EBS: &awsproviderv1.EBSBlockDeviceSpec{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
},
NoDevice: nil,
VirtualName: nil,
},
},
expected: []*ec2.BlockDeviceMapping{
{
DeviceName: &rootDeviceName,
Ebs: &ec2.EbsBlockDevice{
VolumeSize: &volumeSize,
VolumeType: &volumeType,
},
NoDevice: nil,
VirtualName: nil,
},
},
description: "When it gets one blockDevice",
blockDevices: oneBlockDevice,
expected: oneExpectedBlockDevice,
},
{
description: "When it gets two blockDevices",
blockDevices: blockDevices,
expected: twoExpectedDevices,
},
{
description: "When it gets two blockDevices and one with empty device name",
blockDevices: blockDevicesOneEmptyName,
expected: twoExpectedDevices,
},
{
description: "Fail when it gets two blockDevices and two with empty device name",
blockDevices: blockDevicesTwoEmptyNames,
expectedErr: true,
},
}

for _, tc := range testCases {
got, err := getBlockDeviceMappings(tc.blockDevices, "existing-AMI", mockAWSClient)
if err != nil {
t.Errorf("error when calling getBlockDeviceMappings: %v", err)
}
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("Case: %s. Got: %v, expected: %v", tc.description, got, tc.expected)
if tc.expectedErr {
if err == nil {
t.Error("Expected error")
}
} else {
if err != nil {
t.Errorf("error when calling getBlockDeviceMappings: %v", err)
}
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("Got: %v, expected: %v", got, tc.expected)
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/apis/awsprovider/v1beta1/awsproviderconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ type AWSMachineProviderConfig struct {
// should be added once it is created.
LoadBalancers []LoadBalancerReference `json:"loadBalancers,omitempty"`

// BlockDevices is the set of block device mapping associated to this instance
// BlockDevices is the set of block device mapping associated to this instance,
// block device without a name will be used as a root device and only one device without a name is allowed
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
BlockDevices []BlockDeviceMappingSpec `json:"blockDevices,omitempty"`

Expand Down