Skip to content

Commit 4afe3c2

Browse files
authored
Merge pull request #572 from richardcase/439_failure_domain
feat: add failure domain support
2 parents 4c82fa7 + 079b32c commit 4afe3c2

18 files changed

Lines changed: 279 additions & 18 deletions

api/v1alpha2/azurecluster_conversion.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint
4444
return err
4545
}
4646

47+
dst.Status.FailureDomains = restored.Status.FailureDomains
48+
4749
return nil
4850
}
4951

api/v1alpha2/azuremachine_conversion.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint
4040
dst.Spec.Identity = restored.Spec.Identity
4141
}
4242

43+
dst.Spec.FailureDomain = restored.Spec.FailureDomain
44+
4345
return nil
4446
}
4547

api/v1alpha2/azuremachinetemplate_conversion.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nol
3838
dst.Spec.Template.Spec.Identity = restored.Spec.Template.Spec.Identity
3939
}
4040

41+
dst.Spec.Template.Spec.FailureDomain = restored.Spec.Template.Spec.FailureDomain
42+
4143
return nil
4244
}
4345

api/v1alpha2/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha3/azurecluster_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ type AzureClusterSpec struct {
5050
type AzureClusterStatus struct {
5151
Network Network `json:"network,omitempty"`
5252

53+
// FailureDomains specifies the list of unique failure domains for the location/region of the cluster.
54+
// A FailureDomain maps to Availability Zone with an Azure Region (if the region support them). An
55+
// Availability Zone is a separate data center within a region and they can be used to ensure
56+
// the cluster is more resilient to failure.
57+
// See: https://docs.microsoft.com/en-us/azure/availability-zones/az-overview
58+
// This list will be used by Cluster API to try and spread the machines across the failure domains.
59+
FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"`
60+
5361
Bastion VM `json:"bastion,omitempty"`
5462

5563
// Ready is true when the provider resource is ready.

api/v1alpha3/azuremachine_types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ type AzureMachineSpec struct {
3434
// +optional
3535
ProviderID *string `json:"providerID,omitempty"`
3636

37-
VMSize string `json:"vmSize"`
37+
VMSize string `json:"vmSize"`
38+
39+
// FailureDomain is the failure domain unique identifier this Machine should be attached to,
40+
// as defined in Cluster API. This relates to an Azure Availability Zone
41+
FailureDomain *string `json:"failureDomain,omitempty"`
42+
43+
// DEPRECATED: use FailureDomain instead
3844
AvailabilityZone AvailabilityZone `json:"availabilityZone,omitempty"`
3945

4046
// Image is used to provide details of an image to use during VM creation.

api/v1alpha3/zz_generated.deepcopy.go

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/scope/cluster.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,11 @@ func (s *ClusterScope) APIServerPort() int32 {
183183
}
184184
return 6443
185185
}
186+
187+
// SetFailureDomain will set the spec for a for a given key
188+
func (s *ClusterScope) SetFailureDomain(id string, spec clusterv1.FailureDomainSpec) {
189+
if s.AzureCluster.Status.FailureDomains == nil {
190+
s.AzureCluster.Status.FailureDomains = make(clusterv1.FailureDomains, 0)
191+
}
192+
s.AzureCluster.Status.FailureDomains[id] = spec
193+
}

cloud/scope/machine.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,23 @@ func (m *MachineScope) Location() string {
102102
}
103103

104104
// AvailabilityZone returns the AzureMachine Availability Zone.
105+
// Priority for selecting the AZ is
106+
// 1) Machine.Spec.FailureDomain
107+
// 2) AzureMachine.Spec.FailureDomain
108+
// 3) AzureMachine.Spec.AvailabilityZone.ID (This is DEPRECATED)
109+
// 4) No AZ
105110
func (m *MachineScope) AvailabilityZone() string {
106-
return *m.AzureMachine.Spec.AvailabilityZone.ID
111+
if m.Machine.Spec.FailureDomain != nil {
112+
return *m.Machine.Spec.FailureDomain
113+
}
114+
if m.AzureMachine.Spec.FailureDomain != nil {
115+
return *m.AzureMachine.Spec.FailureDomain
116+
}
117+
if m.AzureMachine.Spec.AvailabilityZone.ID != nil {
118+
return *m.AzureMachine.Spec.AvailabilityZone.ID
119+
}
120+
121+
return ""
107122
}
108123

109124
// Name returns the AzureMachine name.

cloud/services/availabilityzones/availabilityzones.go

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package availabilityzones
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"sort"
2223
"strings"
2324

@@ -27,7 +28,7 @@ import (
2728

2829
// Spec input specification for Get/CreateOrUpdate/Delete calls
2930
type Spec struct {
30-
VMSize string
31+
VMSize *string
3132
}
3233

3334
// Get provides information about a availability zones.
@@ -37,26 +38,39 @@ func (s *Service) Get(ctx context.Context, spec interface{}) (interface{}, error
3738
if !ok {
3839
return zones, errors.New("invalid availability zones specification")
3940
}
41+
42+
filter := fmt.Sprintf("location eq '%s'", s.Scope.Location())
43+
4044
// Prefer ListComplete() over List() to automatically traverse pages via iterator.
41-
res, err := s.Client.ListComplete(ctx, "")
45+
res, err := s.Client.ListComplete(ctx, filter)
4246
if err != nil {
4347
return zones, err
4448
}
4549

50+
if skusSpec.VMSize != nil {
51+
return s.filterForVMSizeInLocation(ctx, skusSpec.VMSize, &res)
52+
}
53+
54+
return s.filterUniqueForLocation(ctx, &res)
55+
}
56+
57+
func (s *Service) filterForVMSizeInLocation(ctx context.Context, vmSize *string, res *compute.ResourceSkusResultIterator) ([]string, error) {
58+
var zones []string
59+
4660
for res.NotDone() {
4761
resSku := res.Value()
48-
if strings.EqualFold(*resSku.Name, skusSpec.VMSize) {
62+
if strings.EqualFold(*resSku.Name, *vmSize) {
4963
// Use map for easy deletion and iteration
5064
availableZones := make(map[string]bool)
5165
for _, locationInfo := range *resSku.LocationInfo {
5266
for _, zone := range *locationInfo.Zones {
5367
availableZones[zone] = true
5468
}
55-
if strings.EqualFold(*locationInfo.Location, s.Scope.Location()) {
69+
if strings.EqualFold(*locationInfo.Location, s.Scope.Location()) { //NOTE: this should always be true due to the filter
5670
for _, restriction := range *resSku.Restrictions {
5771
// Can't deploy anything in this subscription in this location. Bail out.
5872
if restriction.Type == compute.Location {
59-
return []string{}, errors.Errorf("rejecting sku: %s in location: %s due to susbcription restriction", skusSpec.VMSize, s.Scope.Location())
73+
return []string{}, errors.Errorf("rejecting sku: %s in location: %s due to susbcription restriction", *vmSize, s.Scope.Location())
6074
}
6175
// May be able to deploy one or more zones to this location.
6276
for _, restrictedZone := range *restriction.RestrictionInfo.Zones {
@@ -74,12 +88,41 @@ func (s *Service) Get(ctx context.Context, spec interface{}) (interface{}, error
7488
}
7589
}
7690
}
77-
err = res.NextWithContext(ctx)
91+
err := res.NextWithContext(ctx)
92+
if err != nil {
93+
return zones, errors.Wrap(err, "could not iterate availability zones")
94+
}
95+
}
96+
97+
return zones, nil
98+
}
99+
100+
func (s *Service) filterUniqueForLocation(ctx context.Context, res *compute.ResourceSkusResultIterator) ([]string, error) {
101+
zones := make([]string, 0)
102+
103+
for res.NotDone() {
104+
resSku := res.Value()
105+
// Use map for easy deletion and iteration
106+
availableZones := make(map[string]bool)
107+
for _, locationInfo := range *resSku.LocationInfo {
108+
for _, zone := range *locationInfo.Zones {
109+
availableZones[zone] = true
110+
}
111+
112+
for availableZone := range availableZones {
113+
if !contains(zones, availableZone) {
114+
zones = append(zones, availableZone)
115+
}
116+
}
117+
}
118+
err := res.NextWithContext(ctx)
78119
if err != nil {
79120
return zones, errors.Wrap(err, "could not iterate availability zones")
80121
}
81122
}
82123

124+
// Lexical sort so comparisons work in tests
125+
sort.Strings(zones)
83126
return zones, nil
84127
}
85128

@@ -94,3 +137,12 @@ func (s *Service) Delete(ctx context.Context, spec interface{}) error {
94137
// Not implemented since there is nothing to delete
95138
return nil
96139
}
140+
141+
func contains(strSlice []string, val string) bool {
142+
for _, c := range strSlice {
143+
if c == val {
144+
return true
145+
}
146+
}
147+
return false
148+
}

0 commit comments

Comments
 (0)