Skip to content

Commit

Permalink
Merge pull request #559 from JoelSpeed/spot-support
Browse files Browse the repository at this point in the history
Add support for Spot VMs
  • Loading branch information
k8s-ci-robot authored Jun 25, 2020
2 parents 6a24966 + 9e423ff commit 4833e50
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 33 deletions.
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"`
}

// 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"`
}

// 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

0 comments on commit 4833e50

Please sign in to comment.