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 Spot VMs #559

Merged
merged 5 commits into from
Jun 25, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions api/v1alpha2/azuremachine_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,27 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
return err
}
if restored.Spec.Identity != "" {
dst.Spec.Identity = restored.Spec.Identity

restoreAzureMachineSpec(&restored.Spec, &dst.Spec)
return nil
}

func restoreAzureMachineSpec(restored, dst *infrav1alpha3.AzureMachineSpec) {
if restored.Identity != "" {
dst.Identity = restored.Identity
}
if len(restored.Spec.UserAssignedIdentities) > 0 {
dst.Spec.UserAssignedIdentities = restored.Spec.UserAssignedIdentities
if len(restored.UserAssignedIdentities) > 0 {
dst.UserAssignedIdentities = restored.UserAssignedIdentities
}
if restored.Spec.AcceleratedNetworking != nil {
dst.Spec.AcceleratedNetworking = restored.Spec.AcceleratedNetworking
if restored.AcceleratedNetworking != nil {
dst.AcceleratedNetworking = restored.AcceleratedNetworking
}

dst.Spec.FailureDomain = restored.Spec.FailureDomain
dst.FailureDomain = restored.FailureDomain

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

// ConvertFrom converts from the Hub version (v1alpha3) to this version.
Expand Down
11 changes: 1 addition & 10 deletions api/v1alpha2/azuremachinetemplate_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,8 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nol
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
return err
}
if restored.Spec.Template.Spec.Identity != "" {
dst.Spec.Template.Spec.Identity = restored.Spec.Template.Spec.Identity
}
if len(restored.Spec.Template.Spec.UserAssignedIdentities) > 0 {
dst.Spec.Template.Spec.UserAssignedIdentities = restored.Spec.Template.Spec.UserAssignedIdentities
}
if restored.Spec.Template.Spec.AcceleratedNetworking != nil {
dst.Spec.Template.Spec.AcceleratedNetworking = restored.Spec.Template.Spec.AcceleratedNetworking
}
dst.Spec.Template.Spec.FailureDomain = restored.Spec.Template.Spec.FailureDomain

restoreAzureMachineSpec(&restored.Spec.Template.Spec, &dst.Spec.Template.Spec)
return nil
}

Expand Down
1 change: 1 addition & 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.

12 changes: 12 additions & 0 deletions api/v1alpha3/azuremachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ type AzureMachineSpec struct {
// +kubebuilder:validation:nullable
// +optional
AcceleratedNetworking *bool `json:"acceleratedNetworking,omitempty"`

// SpotVMOptions allows the ability to specify the Machine should use a Spot VM
// +optional
SpotVMOptions *SpotVMOptions `json:"spotVMOptions,omitempty"`
CecileRobertMichon marked this conversation as resolved.
Show resolved Hide resolved
}

// SpotVMOptions defines the options relevant to running the Machine on Spot VMs
type SpotVMOptions 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"`
CecileRobertMichon marked this conversation as resolved.
Show resolved Hide resolved
}

// AzureMachineStatus defines the observed state of AzureMachine
Expand Down
25 changes: 25 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.

28 changes: 28 additions & 0 deletions cloud/services/virtualmachines/virtualmachines.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"strconv"
"strings"

"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/authorization/mgmt/authorization"
Expand Down Expand Up @@ -49,6 +50,7 @@ type Spec struct {
OSDisk infrav1.OSDisk
CustomData string
UserAssignedIdentities []infrav1.UserAssignedIdentity
SpotVMOptions *infrav1.SpotVMOptions
}

// Get provides information about a virtual machine.
Expand Down Expand Up @@ -101,6 +103,11 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
// Set the cloud provider tag
additionalTags[infrav1.ClusterAzureCloudProviderTagKey(s.MachineScope.Name())] = string(infrav1.ResourceLifecycleOwned)

priority, evictionPolicy, billingProfile, err := getSpotVMOptions(vmSpec.SpotVMOptions)
if err != nil {
return errors.Wrapf(err, "failed to get Spot VM options")
}

virtualMachine := compute.VirtualMachine{
Location: to.StringPtr(s.Scope.Location()),
Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
Expand Down Expand Up @@ -141,6 +148,9 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
},
},
},
Priority: priority,
EvictionPolicy: evictionPolicy,
BillingProfile: billingProfile,
},
}

Expand Down Expand Up @@ -339,6 +349,24 @@ func generateStorageProfile(vmSpec Spec) (*compute.StorageProfile, error) {
return storageProfile, nil
}

func getSpotVMOptions(spotVMOptions *infrav1.SpotVMOptions) (compute.VirtualMachinePriorityTypes, compute.VirtualMachineEvictionPolicyTypes, *compute.BillingProfile, error) {
// Spot VM not requested, return zero values to apply defaults
if spotVMOptions == nil {
return compute.VirtualMachinePriorityTypes(""), compute.VirtualMachineEvictionPolicyTypes(""), nil, nil
}
var billingProfile *compute.BillingProfile
if spotVMOptions.MaxPrice != nil {
maxPrice, err := strconv.ParseFloat(*spotVMOptions.MaxPrice, 64)
if err != nil {
return compute.VirtualMachinePriorityTypes(""), compute.VirtualMachineEvictionPolicyTypes(""), nil, err
}
billingProfile = &compute.BillingProfile{
MaxPrice: &maxPrice,
}
}
return compute.Spot, compute.Deallocate, billingProfile, nil
}

// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
// It will return an error if the system's secure random
Expand Down
88 changes: 73 additions & 15 deletions cloud/services/virtualmachines/virtualmachines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,6 @@ func TestGetVM(t *testing.T) {
}

func TestReconcileVM(t *testing.T) {
g := NewWithT(t)

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bootstrap-data",
Expand All @@ -368,7 +366,7 @@ func TestReconcileVM(t *testing.T) {
machine clusterv1.Machine
machineConfig *infrav1.AzureMachineSpec
azureCluster *infrav1.AzureCluster
expect func(m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder)
expect func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder)
expectedError string
}{
{
Expand Down Expand Up @@ -417,7 +415,7 @@ func TestReconcileVM(t *testing.T) {
},
},
},
expect: func(m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
expect: func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
mnic.Get(gomock.Any(), gomock.Any(), gomock.Any())
m.CreateOrUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
},
Expand Down Expand Up @@ -470,7 +468,7 @@ func TestReconcileVM(t *testing.T) {
},
},
},
expect: func(m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
expect: func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
mnic.Get(gomock.Any(), gomock.Any(), gomock.Any())
m.CreateOrUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
mra.Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
Expand Down Expand Up @@ -525,13 +523,70 @@ func TestReconcileVM(t *testing.T) {
},
},
},
expect: func(m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
expect: func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
mnic.Get(gomock.Any(), gomock.Any(), gomock.Any())
m.CreateOrUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
mra.Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
},
expectedError: "",
},
{
name: "can create a vm on spot",
machine: clusterv1.Machine{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"set": "node"},
},
Spec: clusterv1.MachineSpec{
Bootstrap: clusterv1.Bootstrap{
Data: to.StringPtr("bootstrap-data"),
},
Version: to.StringPtr("1.15.7"),
},
},
machineConfig: &infrav1.AzureMachineSpec{
VMSize: "Standard_B2ms",
Location: "eastus",
Image: image,
SpotVMOptions: &infrav1.SpotVMOptions{},
},
azureCluster: &infrav1.AzureCluster{
Spec: infrav1.AzureClusterSpec{
SubscriptionID: subscriptionID,
NetworkSpec: infrav1.NetworkSpec{
Subnets: infrav1.Subnets{
&infrav1.SubnetSpec{
Name: "subnet-1",
},
&infrav1.SubnetSpec{},
},
},
},
Status: infrav1.AzureClusterStatus{
Network: infrav1.Network{
SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{
infrav1.SecurityGroupControlPlane: {
ID: "1",
},
infrav1.SecurityGroupNode: {
ID: "2",
},
},
APIServerIP: infrav1.PublicIP{
DNSName: "azure-test-dns",
},
},
},
},
expect: func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
mnic.Get(gomock.Any(), gomock.Any(), gomock.Any())
m.CreateOrUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_, _, _ interface{}, vm compute.VirtualMachine) {
g.Expect(vm.Priority).To(Equal(compute.Spot))
g.Expect(vm.EvictionPolicy).To(Equal(compute.Deallocate))
g.Expect(vm.BillingProfile).To(BeNil())
})
},
expectedError: "",
},
{
name: "vm creation fails",
machine: clusterv1.Machine{
Expand Down Expand Up @@ -578,7 +633,7 @@ func TestReconcileVM(t *testing.T) {
},
},
},
expect: func(m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
expect: func(g *WithT, m *mock_virtualmachines.MockClientMockRecorder, mnic *mock_networkinterfaces.MockClientMockRecorder, mpip *mock_publicips.MockClientMockRecorder, mra *mock_roleassignments.MockClientMockRecorder) {
mnic.Get(gomock.Any(), gomock.Any(), gomock.Any())
m.CreateOrUpdate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))
},
Expand All @@ -588,6 +643,8 @@ func TestReconcileVM(t *testing.T) {

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

mockCtrl := gomock.NewController(t)
vmMock := mock_virtualmachines.NewMockClient(mockCtrl)
interfaceMock := mock_networkinterfaces.NewMockClient(mockCtrl)
Expand Down Expand Up @@ -639,7 +696,7 @@ func TestReconcileVM(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())

machineScope.AzureMachine.Spec = *tc.machineConfig
tc.expect(vmMock.EXPECT(), interfaceMock.EXPECT(), publicIPMock.EXPECT(), roleAssignmentMock.EXPECT())
tc.expect(g, vmMock.EXPECT(), interfaceMock.EXPECT(), publicIPMock.EXPECT(), roleAssignmentMock.EXPECT())

clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
AzureClients: scope.AzureClients{
Expand All @@ -661,13 +718,14 @@ func TestReconcileVM(t *testing.T) {
}

vmSpec := &Spec{
Name: machineScope.Name(),
NICName: "test-nic",
SSHKeyData: "fake-key",
Size: machineScope.AzureMachine.Spec.VMSize,
OSDisk: machineScope.AzureMachine.Spec.OSDisk,
Image: machineScope.AzureMachine.Spec.Image,
CustomData: *machineScope.Machine.Spec.Bootstrap.Data,
Name: machineScope.Name(),
NICName: "test-nic",
SSHKeyData: "fake-key",
Size: machineScope.AzureMachine.Spec.VMSize,
OSDisk: machineScope.AzureMachine.Spec.OSDisk,
Image: machineScope.AzureMachine.Spec.Image,
CustomData: *machineScope.Machine.Spec.Bootstrap.Data,
SpotVMOptions: machineScope.AzureMachine.Spec.SpotVMOptions,
}
err = s.Reconcile(context.TODO(), vmSpec)
if tc.expectedError != "" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,15 @@ spec:
description: ProviderID is the unique identifier as specified by the
cloud provider.
type: string
spotVMOptions:
description: SpotVMOptions allows the ability to specify the Machine
should use a Spot VM
properties:
maxPrice:
description: MaxPrice defines the maximum price the user is willing
to pay for Spot VM instances
type: number
type: object
sshPublicKey:
type: string
userAssignedIdentities:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,15 @@ spec:
description: ProviderID is the unique identifier as specified
by the cloud provider.
type: string
spotVMOptions:
description: SpotVMOptions allows the ability to specify the
Machine should use a Spot VM
properties:
maxPrice:
description: MaxPrice defines the maximum price the user
is willing to pay for Spot VM instances
type: number
type: object
sshPublicKey:
type: string
userAssignedIdentities:
Expand Down
1 change: 1 addition & 0 deletions controllers/azuremachine_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ func (s *azureMachineService) reconcileVirtualMachine(ctx context.Context, nicNa
Zone: vmZone,
Identity: s.machineScope.AzureMachine.Spec.Identity,
UserAssignedIdentities: s.machineScope.AzureMachine.Spec.UserAssignedIdentities,
SpotVMOptions: s.machineScope.AzureMachine.Spec.SpotVMOptions,
}

err = s.virtualMachinesSvc.Reconcile(ctx, vmSpec)
Expand Down
Loading