Skip to content

Commit 90e00da

Browse files
authored
Merge pull request #340 from CecileRobertMichon/byo-vnet
✨Add support for BYO vnet
2 parents 6df08db + ef8b3aa commit 90e00da

File tree

25 files changed

+765
-186
lines changed

25 files changed

+765
-186
lines changed

api/v1alpha2/types.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,21 @@ type Network struct {
9090

9191
// NetworkSpec encapsulates all things related to Azure network.
9292
type NetworkSpec struct {
93-
// Vnet configuration.
93+
// Vnet is the configuration for the Azure virtual network.
9494
// +optional
9595
Vnet VnetSpec `json:"vnet,omitempty"`
9696

97-
// Subnets configuration.
97+
// Subnets is the configuration for the control-plane subnet and the node subnet.
9898
// +optional
9999
Subnets Subnets `json:"subnets,omitempty"`
100100
}
101101

102102
// VnetSpec configures an Azure virtual network.
103103
type VnetSpec struct {
104+
// ResourceGroup is the name of the resource group of the existing virtual network
105+
// or the resource group where a managed virtual network should be created.
106+
ResourceGroup string `json:"resourceGroup,omitempty"`
107+
104108
// ID is the identifier of the virtual network this provider should use to create resources.
105109
ID string `json:"id,omitempty"`
106110

@@ -114,9 +118,9 @@ type VnetSpec struct {
114118
Tags Tags `json:"tags,omitempty"`
115119
}
116120

117-
// IsManaged returns true if the vnet is unmanaged.
121+
// IsManaged returns true if the vnet is managed.
118122
func (v *VnetSpec) IsManaged(clusterName string) bool {
119-
return v.ID != "" && !v.Tags.HasOwned(clusterName)
123+
return v.ID == "" || v.Tags.HasOwned(clusterName)
120124
}
121125

122126
// Subnets is a slice of Subnet.
@@ -147,9 +151,9 @@ var (
147151

148152
// SecurityGroup defines an Azure security group.
149153
type SecurityGroup struct {
150-
ID string `json:"id"`
151-
Name string `json:"name"`
152-
IngressRules IngressRules `json:"ingressRule"`
154+
ID string `json:"id,omitempty"`
155+
Name string `json:"name,omitempty"`
156+
IngressRules IngressRules `json:"ingressRule,omitempty"`
153157
Tags Tags `json:"tags,omitempty"`
154158
}
155159

@@ -433,22 +437,37 @@ type ManagedDisk struct {
433437
StorageAccountType string `json:"storageAccountType"`
434438
}
435439

440+
// SubnetRole defines the unique role of a subnet.
441+
type SubnetRole string
442+
443+
var (
444+
// SubnetNode defines a Kubernetes workload node role
445+
SubnetNode = SubnetRole(Node)
446+
447+
// SubnetControlPlane defines a Kubernetes control plane node role
448+
SubnetControlPlane = SubnetRole(ControlPlane)
449+
)
450+
436451
// SubnetSpec configures an Azure subnet.
437452
type SubnetSpec struct {
453+
// Role defines the subnet role (eg. Node, ControlPlane)
454+
Role SubnetRole `json:"role,omitempty"`
455+
438456
// ID defines a unique identifier to reference this resource.
439457
ID string `json:"id,omitempty"`
440458

441459
// Name defines a name for the subnet resource.
442460
Name string `json:"name"`
443461

444-
// VnetID defines the ID of the virtual network this subnet should be built in.
445-
VnetID string `json:"vnetId"`
446-
447462
// CidrBlock is the CIDR block to be used when the provider creates a managed Vnet.
448463
CidrBlock string `json:"cidrBlock,omitempty"`
449464

465+
// InternalLBIPAddress is the IP address that will be used as the internal LB private IP.
466+
// For the control plane subnet only.
467+
InternalLBIPAddress string `json:"internalLBIPAddress,omitempty"`
468+
450469
// SecurityGroup defines the NSG (network security group) that should be attached to this subnet.
451-
SecurityGroup SecurityGroup `json:"securityGroup"`
470+
SecurityGroup SecurityGroup `json:"securityGroup,omitempty"`
452471
}
453472

454473
const (

cloud/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"github.com/Azure/go-autorest/autorest"
2121
)
2222

23-
// ResourceNotFound parses the error to check if its a resource not found
23+
// ResourceNotFound parses the error to check if it's a resource not found
2424
func ResourceNotFound(err error) bool {
2525
if derr, ok := err.(autorest.DetailedError); ok && derr.StatusCode == 404 {
2626
return true

cloud/scope/cluster.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,42 @@ func (s *ClusterScope) Subnets() infrav1.Subnets {
9999
return s.AzureCluster.Spec.NetworkSpec.Subnets
100100
}
101101

102+
// ControlPlaneSubnet returns the cluster control plane subnet.
103+
func (s *ClusterScope) ControlPlaneSubnet() *infrav1.SubnetSpec {
104+
for _, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
105+
if sn.Role == infrav1.SubnetControlPlane {
106+
return sn
107+
}
108+
}
109+
if len(s.AzureCluster.Spec.NetworkSpec.Subnets) > 0 {
110+
return s.AzureCluster.Spec.NetworkSpec.Subnets[0]
111+
}
112+
return nil
113+
}
114+
115+
// NodeSubnet returns the cluster node subnet.
116+
func (s *ClusterScope) NodeSubnet() *infrav1.SubnetSpec {
117+
for _, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
118+
if sn.Role == infrav1.SubnetNode {
119+
return sn
120+
}
121+
}
122+
if len(s.AzureCluster.Spec.NetworkSpec.Subnets) > 1 {
123+
return s.AzureCluster.Spec.NetworkSpec.Subnets[1]
124+
}
125+
return nil
126+
}
127+
102128
// SecurityGroups returns the cluster security groups as a map, it creates the map if empty.
103129
func (s *ClusterScope) SecurityGroups() map[infrav1.SecurityGroupRole]infrav1.SecurityGroup {
104130
return s.AzureCluster.Status.Network.SecurityGroups
105131
}
106132

133+
// ResourceGroup returns the cluster resource group.
134+
func (s *ClusterScope) ResourceGroup() string {
135+
return s.AzureCluster.Spec.ResourceGroup
136+
}
137+
107138
// Name returns the cluster name.
108139
func (s *ClusterScope) Name() string {
109140
return s.Cluster.Name

cloud/services/disks/disks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ func (s *Service) Delete(ctx context.Context, spec interface{}) error {
4646
return errors.New("Invalid disk specification")
4747
}
4848
klog.V(2).Infof("deleting disk %s", diskSpec.Name)
49-
err := s.Client.Delete(ctx, s.Scope.AzureCluster.Spec.ResourceGroup, diskSpec.Name)
49+
err := s.Client.Delete(ctx, s.Scope.ResourceGroup(), diskSpec.Name)
5050
if err != nil && azure.ResourceNotFound(err) {
5151
// already deleted
5252
return nil
5353
}
5454
if err != nil {
55-
return errors.Wrapf(err, "failed to delete disk %s in resource group %s", diskSpec.Name, s.Scope.AzureCluster.Spec.ResourceGroup)
55+
return errors.Wrapf(err, "failed to delete disk %s in resource group %s", diskSpec.Name, s.Scope.ResourceGroup())
5656
}
5757

5858
klog.V(2).Infof("successfully deleted disk %s", diskSpec.Name)

cloud/services/groups/groups.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030

3131
// Get provides information about a resource group.
3232
func (s *Service) Get(ctx context.Context, spec interface{}) (resources.Group, error) {
33-
return s.Client.Get(ctx, s.Scope.AzureCluster.Spec.ResourceGroup)
33+
return s.Client.Get(ctx, s.Scope.ResourceGroup())
3434
}
3535

3636
// Reconcile gets/creates/updates a resource group.
@@ -39,19 +39,19 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
3939
// resource group already exists, skip creation
4040
return nil
4141
}
42-
klog.V(2).Infof("creating resource group %s", s.Scope.AzureCluster.Spec.ResourceGroup)
42+
klog.V(2).Infof("creating resource group %s", s.Scope.ResourceGroup())
4343
group := resources.Group{
4444
Location: to.StringPtr(s.Scope.Location()),
4545
Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
4646
ClusterName: s.Scope.Name(),
4747
Lifecycle: infrav1.ResourceLifecycleOwned,
48-
Name: to.StringPtr(s.Scope.AzureCluster.Spec.ResourceGroup),
48+
Name: to.StringPtr(s.Scope.ResourceGroup()),
4949
Role: to.StringPtr(infrav1.CommonRoleTagValue),
5050
Additional: s.Scope.AdditionalTags(),
5151
})),
5252
}
53-
_, err := s.Client.CreateOrUpdate(ctx, s.Scope.AzureCluster.Spec.ResourceGroup, group)
54-
klog.V(2).Infof("successfully created resource group %s", s.Scope.AzureCluster.Spec.ResourceGroup)
53+
_, err := s.Client.CreateOrUpdate(ctx, s.Scope.ResourceGroup(), group)
54+
klog.V(2).Infof("successfully created resource group %s", s.Scope.ResourceGroup())
5555
return err
5656
}
5757

@@ -66,17 +66,17 @@ func (s *Service) Delete(ctx context.Context, spec interface{}) error {
6666
s.Scope.V(4).Info("Skipping resource group deletion in unmanaged mode")
6767
return nil
6868
}
69-
klog.V(2).Infof("deleting resource group %s", s.Scope.AzureCluster.Spec.ResourceGroup)
70-
err = s.Client.Delete(ctx, s.Scope.AzureCluster.Spec.ResourceGroup)
69+
klog.V(2).Infof("deleting resource group %s", s.Scope.ResourceGroup())
70+
err = s.Client.Delete(ctx, s.Scope.ResourceGroup())
7171
if err != nil && azure.ResourceNotFound(err) {
7272
// already deleted
7373
return nil
7474
}
7575
if err != nil {
76-
return errors.Wrapf(err, "failed to delete resource group %s", s.Scope.AzureCluster.Spec.ResourceGroup)
76+
return errors.Wrapf(err, "failed to delete resource group %s", s.Scope.ResourceGroup())
7777
}
7878

79-
klog.V(2).Infof("successfully deleted resource group %s", s.Scope.AzureCluster.Spec.ResourceGroup)
79+
klog.V(2).Infof("successfully deleted resource group %s", s.Scope.ResourceGroup())
8080
return nil
8181
}
8282

cloud/services/internalloadbalancers/internalloadbalancers.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,18 @@ import (
3131
type Spec struct {
3232
Name string
3333
SubnetName string
34+
SubnetCidr string
3435
VnetName string
3536
IPAddress string
3637
}
3738

3839
// Get provides information about an internal load balancer.
39-
func (s *Service) Get(ctx context.Context, spec interface{}) (interface{}, error) {
40+
func (s *Service) Get(ctx context.Context, spec interface{}) (network.LoadBalancer, error) {
4041
internalLBSpec, ok := spec.(*Spec)
4142
if !ok {
4243
return network.LoadBalancer{}, errors.New("invalid internal load balancer specification")
4344
}
44-
//lbName := fmt.Sprintf("%s-api-internallb", s.Scope.Cluster.Name)
45-
lb, err := s.Client.Get(ctx, s.Scope.AzureCluster.Spec.ResourceGroup, internalLBSpec.Name)
46-
if err != nil && azure.ResourceNotFound(err) {
47-
return nil, errors.Wrapf(err, "load balancer %s not found", internalLBSpec.Name)
48-
} else if err != nil {
49-
return lb, err
50-
}
51-
return lb, nil
45+
return s.Client.Get(ctx, s.Scope.ResourceGroup(), internalLBSpec.Name)
5246
}
5347

5448
// Reconcile gets/creates/updates an internal load balancer.
@@ -61,20 +55,38 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
6155
probeName := "tcpHTTPSProbe"
6256
frontEndIPConfigName := "controlplane-internal-lbFrontEnd"
6357
backEndAddressPoolName := "controlplane-internal-backEndPool"
64-
idPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers", s.Scope.SubscriptionID, s.Scope.AzureCluster.Spec.ResourceGroup)
58+
idPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers", s.Scope.SubscriptionID, s.Scope.ResourceGroup())
6559
lbName := internalLBSpec.Name
60+
var privateIP string
61+
62+
internalLB, err := s.Get(ctx, internalLBSpec)
63+
if err == nil {
64+
ipConfigs := internalLB.LoadBalancerPropertiesFormat.FrontendIPConfigurations
65+
if ipConfigs != nil && len(*ipConfigs) > 0 {
66+
privateIP = to.String((*ipConfigs)[0].FrontendIPConfigurationPropertiesFormat.PrivateIPAddress)
67+
}
68+
} else if azure.ResourceNotFound(err) {
69+
klog.V(2).Infof("internalLB %s not found in RG %s", internalLBSpec.Name, s.Scope.ResourceGroup())
70+
privateIP, err = s.getAvailablePrivateIP(ctx, s.Scope.Vnet().ResourceGroup, internalLBSpec.VnetName, internalLBSpec.SubnetCidr, internalLBSpec.IPAddress)
71+
if err != nil {
72+
return err
73+
}
74+
klog.V(2).Infof("setting internal load balancer IP to %s", privateIP)
75+
} else {
76+
return errors.Wrap(err, "failed to look for existing internal LB")
77+
}
6678

6779
klog.V(2).Infof("getting subnet %s", internalLBSpec.SubnetName)
68-
subnet, err := s.SubnetsClient.Get(ctx, s.Scope.AzureCluster.Spec.ResourceGroup, internalLBSpec.VnetName, internalLBSpec.SubnetName)
80+
subnet, err := s.SubnetsClient.Get(ctx, s.Scope.Vnet().ResourceGroup, internalLBSpec.VnetName, internalLBSpec.SubnetName)
6981
if err != nil {
70-
return err
82+
return errors.Wrap(err, "failed to get subnet")
7183
}
7284

7385
klog.V(2).Infof("successfully got subnet %s", internalLBSpec.SubnetName)
7486

7587
// https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-standard-availability-zones#zone-redundant-by-default
7688
err = s.Client.CreateOrUpdate(ctx,
77-
s.Scope.AzureCluster.Spec.ResourceGroup,
89+
s.Scope.ResourceGroup(),
7890
lbName,
7991
network.LoadBalancer{
8092
Sku: &network.LoadBalancerSku{Name: network.LoadBalancerSkuNameStandard},
@@ -86,7 +98,7 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
8698
FrontendIPConfigurationPropertiesFormat: &network.FrontendIPConfigurationPropertiesFormat{
8799
PrivateIPAllocationMethod: network.Static,
88100
Subnet: &subnet,
89-
PrivateIPAddress: to.StringPtr(internalLBSpec.IPAddress),
101+
PrivateIPAddress: to.StringPtr(privateIP),
90102
},
91103
},
92104
},
@@ -146,14 +158,38 @@ func (s *Service) Delete(ctx context.Context, spec interface{}) error {
146158
return errors.New("invalid internal load balancer specification")
147159
}
148160
klog.V(2).Infof("deleting internal load balancer %s", internalLBSpec.Name)
149-
err := s.Client.Delete(ctx, s.Scope.AzureCluster.Spec.ResourceGroup, internalLBSpec.Name)
161+
err := s.Client.Delete(ctx, s.Scope.ResourceGroup(), internalLBSpec.Name)
150162
if err != nil && azure.ResourceNotFound(err) {
151163
// already deleted
152164
return nil
153165
}
154166
if err != nil {
155-
return errors.Wrapf(err, "failed to delete internal load balancer %s in resource group %s", internalLBSpec.Name, s.Scope.AzureCluster.Spec.ResourceGroup)
167+
return errors.Wrapf(err, "failed to delete internal load balancer %s in resource group %s", internalLBSpec.Name, s.Scope.ResourceGroup())
156168
}
157169
klog.V(2).Infof("successfully deleted internal load balancer %s", internalLBSpec.Name)
158170
return nil
159171
}
172+
173+
// getAvailablePrivateIP checks if the desired private IP address is available in a virtual network.
174+
// If the IP address is taken or empty, it will make an attempt to find an available IP in the same subnet
175+
func (s *Service) getAvailablePrivateIP(ctx context.Context, resourceGroup, vnetName, subnetCIDR, PreferredIPAddress string) (string, error) {
176+
ip := PreferredIPAddress
177+
if ip == "" {
178+
ip = azure.DefaultInternalLBIPAddress
179+
if subnetCIDR != azure.DefaultControlPlaneSubnetCIDR {
180+
// If the user provided a custom subnet CIDR without providing a private IP, try finding an available IP in the subnet space
181+
ip = subnetCIDR[0:7] + "0"
182+
}
183+
}
184+
result, err := s.VirtualNetworksClient.CheckIPAddressAvailability(ctx, resourceGroup, vnetName, ip)
185+
if err != nil {
186+
return "", errors.Wrap(err, "failed to check IP availability")
187+
}
188+
if !to.Bool(result.Available) {
189+
if len(to.StringSlice(result.AvailableIPAddresses)) == 0 {
190+
return "", errors.Errorf("IP %s is not available in vnet %s and there were no other available IPs found", ip, vnetName)
191+
}
192+
ip = to.StringSlice(result.AvailableIPAddresses)[0]
193+
}
194+
return ip, nil
195+
}

cloud/services/internalloadbalancers/service.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ package internalloadbalancers
1919
import (
2020
"sigs.k8s.io/cluster-api-provider-azure/cloud/scope"
2121
"sigs.k8s.io/cluster-api-provider-azure/cloud/services/subnets"
22+
"sigs.k8s.io/cluster-api-provider-azure/cloud/services/virtualnetworks"
2223
)
2324

2425
// Service provides operations on azure resources
2526
type Service struct {
2627
Scope *scope.ClusterScope
2728
Client
28-
SubnetsClient subnets.Client
29+
SubnetsClient subnets.Client
30+
VirtualNetworksClient virtualnetworks.Client
2931
}
3032

3133
// NewService creates a new service.
3234
func NewService(scope *scope.ClusterScope) *Service {
3335
return &Service{
34-
Scope: scope,
35-
Client: NewClient(scope.SubscriptionID, scope.Authorizer),
36-
SubnetsClient: subnets.NewClient(scope.SubscriptionID, scope.Authorizer),
36+
Scope: scope,
37+
Client: NewClient(scope.SubscriptionID, scope.Authorizer),
38+
SubnetsClient: subnets.NewClient(scope.SubscriptionID, scope.Authorizer),
39+
VirtualNetworksClient: virtualnetworks.NewClient(scope.SubscriptionID, scope.Authorizer),
3740
}
3841
}

0 commit comments

Comments
 (0)