Skip to content

Commit

Permalink
Merge pull request #1868 from alexander-demichev/spot
Browse files Browse the repository at this point in the history
✨ Add Spot instances support
  • Loading branch information
k8s-ci-robot authored Aug 14, 2020
2 parents ab5ddd6 + 77d493d commit 66d77f4
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 0 deletions.
4 changes: 4 additions & 0 deletions api/v1alpha2/awsmachine_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func restoreAWSMachineSpec(restored, dst *infrav1alpha3.AWSMachineSpec) {

// manual conversion for UncompressedUserData
dst.UncompressedUserData = restored.UncompressedUserData

if restored.SpotMarketOptions != nil {
dst.SpotMarketOptions = restored.SpotMarketOptions.DeepCopy()
}
}

// ConvertFrom converts from the Hub version (v1alpha3) to this version.
Expand Down
2 changes: 2 additions & 0 deletions api/v1alpha2/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions api/v1alpha3/awsmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ type AWSMachineSpec struct {
// CloudInit is used.
// +optional
CloudInit CloudInit `json:"cloudInit,omitempty"`

// SpotMarketOptions allows users to configure instances to be run using AWS Spot instances.
// +optional
SpotMarketOptions *SpotMarketOptions `json:"spotMarketOptions,omitempty"`
}

// CloudInit defines options related to the bootstrapping systems where
Expand Down
13 changes: 13 additions & 0 deletions api/v1alpha3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ type Instance struct {

// Availability zone of instance
AvailabilityZone string `json:"availabilityZone,omitempty"`

// SpotMarketOptions option for configuring instances to be run using AWS Spot instances.
SpotMarketOptions *SpotMarketOptions `json:"spotMarketOptions,omitempty"`
}

// RootVolume encapsulates the configuration options for the root volume
Expand Down Expand Up @@ -653,3 +656,13 @@ type RootVolume struct {
// +optional
EncryptionKey string `json:"encryptionKey,omitempty"`
}

// SpotMarketOptions defines the options available to a user when configuring
// Machines to run on Spot instances.
// Most users should provide an empty struct.
type SpotMarketOptions struct {
// MaxPrice defines the maximum price the user is willing to pay for Spot VM instances
// +optional
// +kubebuilder:validation:Type=number
MaxPrice *string `json:"maxPrice,omitempty"`
}
30 changes: 30 additions & 0 deletions api/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,13 @@ spec:
items:
type: string
type: array
spotMarketOptions:
description: SpotMarketOptions option for configuring instances to be run using AWS Spot instances.
properties:
maxPrice:
description: MaxPrice defines the maximum price the user is willing to pay for Spot VM instances
type: number
type: object
sshKeyName:
description: The name of the SSH key pair.
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,13 @@ spec:
required:
- size
type: object
spotMarketOptions:
description: SpotMarketOptions allows users to configure instances to be run using AWS Spot instances.
properties:
maxPrice:
description: MaxPrice defines the maximum price the user is willing to pay for Spot VM instances
type: number
type: object
sshKeyName:
description: SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name)
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ spec:
required:
- size
type: object
spotMarketOptions:
description: SpotMarketOptions allows users to configure instances to be run using AWS Spot instances.
properties:
maxPrice:
description: MaxPrice defines the maximum price the user is willing to pay for Spot VM instances
type: number
type: object
sshKeyName:
description: SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name)
type: string
Expand Down
34 changes: 34 additions & 0 deletions pkg/cloud/services/ec2/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte) (*i
}
}

input.SpotMarketOptions = scope.AWSMachine.Spec.SpotMarketOptions

s.scope.V(2).Info("Running instance", "machine-role", scope.Role())
out, err := s.runInstance(scope.Role(), input)
if err != nil {
Expand Down Expand Up @@ -449,6 +451,8 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan
input.TagSpecifications = append(input.TagSpecifications, spec)
}

input.InstanceMarketOptions = getInstanceMarketOptionsRequest(i.SpotMarketOptions)

out, err := s.EC2Client.RunInstances(input)
if err != nil {
return nil, errors.Wrap(err, "failed to run instance")
Expand Down Expand Up @@ -790,3 +794,33 @@ func containsGroup(list []string, strToSearch string) bool {
}
return false
}

func getInstanceMarketOptionsRequest(spotMarketOptions *infrav1.SpotMarketOptions) *ec2.InstanceMarketOptionsRequest {
if spotMarketOptions == nil {
// Instance is not a Spot instance
return nil
}

// Set required values for Spot instances
spotOptions := &ec2.SpotMarketOptions{}

// The following two options ensure that:
// - If an instance is interrupted, it is terminated rather than hibernating or stopping
// - No replacement instance will be created if the instance is interrupted
// - If the spot request cannot immediately be fulfilled, it will not be created
// This behaviour should satisfy the 1:1 mapping of Machines to Instances as
// assumed by the Cluster API.
spotOptions.SetInstanceInterruptionBehavior(ec2.InstanceInterruptionBehaviorTerminate)
spotOptions.SetSpotInstanceType(ec2.SpotInstanceTypeOneTime)

maxPrice := spotMarketOptions.MaxPrice
if maxPrice != nil && *maxPrice != "" {
spotOptions.SetMaxPrice(*maxPrice)
}

instanceMarketOptionsRequest := &ec2.InstanceMarketOptionsRequest{}
instanceMarketOptionsRequest.SetMarketType(ec2.MarketTypeSpot)
instanceMarketOptionsRequest.SetSpotOptions(spotOptions)

return instanceMarketOptionsRequest
}
62 changes: 62 additions & 0 deletions pkg/cloud/services/ec2/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package ec2

import (
"reflect"
"testing"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -1078,6 +1079,67 @@ func TestCreateInstance(t *testing.T) {
}
}

func TestGetInstanceMarketOptionsRequest(t *testing.T) {
testCases := []struct {
name string
spotMarketOptions *infrav1.SpotMarketOptions
expectedRequest *ec2.InstanceMarketOptionsRequest
}{
{
name: "with no Spot options specified",
spotMarketOptions: nil,
expectedRequest: nil,
},
{
name: "with an empty Spot options specified",
spotMarketOptions: &infrav1.SpotMarketOptions{},
expectedRequest: &ec2.InstanceMarketOptionsRequest{
MarketType: aws.String(ec2.MarketTypeSpot),
SpotOptions: &ec2.SpotMarketOptions{
InstanceInterruptionBehavior: aws.String(ec2.InstanceInterruptionBehaviorTerminate),
SpotInstanceType: aws.String(ec2.SpotInstanceTypeOneTime),
},
},
},
{
name: "with an empty MaxPrice specified",
spotMarketOptions: &infrav1.SpotMarketOptions{
MaxPrice: aws.String(""),
},
expectedRequest: &ec2.InstanceMarketOptionsRequest{
MarketType: aws.String(ec2.MarketTypeSpot),
SpotOptions: &ec2.SpotMarketOptions{
InstanceInterruptionBehavior: aws.String(ec2.InstanceInterruptionBehaviorTerminate),
SpotInstanceType: aws.String(ec2.SpotInstanceTypeOneTime),
},
},
},
{
name: "with a valid MaxPrice specified",
spotMarketOptions: &infrav1.SpotMarketOptions{
MaxPrice: aws.String("0.01"),
},
expectedRequest: &ec2.InstanceMarketOptionsRequest{
MarketType: aws.String(ec2.MarketTypeSpot),
SpotOptions: &ec2.SpotMarketOptions{
InstanceInterruptionBehavior: aws.String(ec2.InstanceInterruptionBehaviorTerminate),
SpotInstanceType: aws.String(ec2.SpotInstanceTypeOneTime),
MaxPrice: aws.String("0.01"),
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
request := getInstanceMarketOptionsRequest(tc.spotMarketOptions)
if !reflect.DeepEqual(request, tc.expectedRequest) {
t.Errorf("Case: %s. Got: %v, expected: %v", tc.name, request, tc.expectedRequest)
}
})
}
}

func setupScheme() (*runtime.Scheme, error) {
scheme := runtime.NewScheme()
if err := clusterv1.AddToScheme(scheme); err != nil {
Expand Down

0 comments on commit 66d77f4

Please sign in to comment.