Skip to content

Commit 2522ab5

Browse files
authored
[feat] Adding ability to BYO VPC and configure with it (#708)
- Updated the docs to explain the feature for both VPC and Firewall. - Updated Firewall Preflight check to check for firewall id and verify
1 parent e787951 commit 2522ab5

22 files changed

+3533
-99
lines changed

api/v1alpha2/linodecluster_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ type LinodeClusterSpec struct {
4646
// +optional
4747
VPCRef *corev1.ObjectReference `json:"vpcRef,omitempty"`
4848

49+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
50+
// VPCID is the ID of an existing VPC in Linode. This allows using a VPC that is not managed by CAPL.
51+
// +optional
52+
VPCID *int `json:"vpcID,omitempty"`
53+
4954
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
5055
// +optional
5156
// NodeBalancerFirewallRef is a reference to a NodeBalancer Firewall object. This makes the linode use the specified NodeBalancer Firewall.

api/v1alpha2/linodemachine_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ type LinodeMachineSpec struct {
106106
// VPCRef is a reference to a LinodeVPC resource. If specified, this takes precedence over
107107
// the cluster-level VPC configuration for multi-region support.
108108
VPCRef *corev1.ObjectReference `json:"vpcRef,omitempty"`
109+
110+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
111+
// VPCID is the ID of an existing VPC in Linode. This allows using a VPC that is not managed by CAPL.
112+
// +optional
113+
VPCID *int `json:"vpcID,omitempty"`
109114
}
110115

111116
// InstanceDisk defines a list of disks to use for an instance

api/v1alpha2/zz_generated.deepcopy.go

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

cloud/services/loadbalancers.go

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,88 @@ const (
2222
DefaultKonnectivityLBPort = 8132
2323
)
2424

25+
// FindSubnet selects a subnet from the provided subnets based on the subnet name
26+
// It handles both direct VPC subnets and VPCRef subnets
27+
// If subnet name is provided, it looks for a matching subnet; otherwise, it uses the first subnet
28+
// Returns the subnet ID and any error encountered
29+
func FindSubnet(subnetName string, isDirectVPC bool, subnets interface{}) (int, error) {
30+
var subnetID int
31+
var err error
32+
33+
// Different handling based on whether we're dealing with a direct VPC or VPCRef
34+
if isDirectVPC {
35+
subnetID, err = findDirectVPCSubnet(subnetName, subnets)
36+
} else {
37+
subnetID, err = findVPCRefSubnet(subnetName, subnets)
38+
}
39+
40+
if err != nil {
41+
return 0, err
42+
}
43+
44+
// Validate the selected subnet ID
45+
if subnetID == 0 {
46+
return 0, errors.New("invalid subnet ID: selected subnet ID is 0")
47+
}
48+
49+
return subnetID, nil
50+
}
51+
52+
// findDirectVPCSubnet finds a subnet in direct VPC subnets
53+
func findDirectVPCSubnet(subnetName string, subnets interface{}) (int, error) {
54+
vpcSubnets, ok := subnets.([]linodego.VPCSubnet)
55+
if !ok {
56+
return 0, fmt.Errorf("invalid subnet data type for direct VPC: expected []linodego.VPCSubnet")
57+
}
58+
59+
if len(vpcSubnets) == 0 {
60+
return 0, errors.New("no subnets found in VPC")
61+
}
62+
63+
return selectSubnet(subnetName, vpcSubnets, func(subnet linodego.VPCSubnet) (string, int) {
64+
return subnet.Label, subnet.ID
65+
})
66+
}
67+
68+
// findVPCRefSubnet finds a subnet in VPCRef subnets
69+
func findVPCRefSubnet(subnetName string, subnets interface{}) (int, error) {
70+
vpcRefSubnets, ok := subnets.([]v1alpha2.VPCSubnetCreateOptions)
71+
if !ok {
72+
return 0, fmt.Errorf("invalid subnet data type for VPC reference: expected []v1alpha2.VPCSubnetCreateOptions")
73+
}
74+
75+
if len(vpcRefSubnets) == 0 {
76+
return 0, errors.New("no subnets found in LinodeVPC")
77+
}
78+
79+
return selectSubnet(subnetName, vpcRefSubnets, func(subnet v1alpha2.VPCSubnetCreateOptions) (string, int) {
80+
return subnet.Label, subnet.SubnetID
81+
})
82+
}
83+
84+
// selectSubnet is a generic helper to select a subnet by name or use the first one
85+
func selectSubnet[T any](subnetName string, subnets []T, getProps func(T) (string, int)) (int, error) {
86+
if len(subnets) == 0 {
87+
return 0, errors.New("no subnets available in the VPC")
88+
}
89+
90+
// If subnet name specified, find matching subnet
91+
if subnetName != "" {
92+
for _, subnet := range subnets {
93+
label, id := getProps(subnet)
94+
if label == subnetName {
95+
return id, nil
96+
}
97+
}
98+
// Keep the original error message format for compatibility with tests
99+
return 0, fmt.Errorf("subnet with label %s not found in VPC", subnetName)
100+
}
101+
102+
// Use the first subnet when no specific name is provided
103+
_, id := getProps(subnets[0])
104+
return id, nil
105+
}
106+
25107
// EnsureNodeBalancer creates a new NodeBalancer if one doesn't exist or returns the existing NodeBalancer
26108
func EnsureNodeBalancer(ctx context.Context, clusterScope *scope.ClusterScope, logger logr.Logger) (*linodego.NodeBalancer, error) {
27109
nbID := clusterScope.LinodeCluster.Spec.Network.NodeBalancerID
@@ -44,7 +126,7 @@ func EnsureNodeBalancer(ctx context.Context, clusterScope *scope.ClusterScope, l
44126
}
45127

46128
// if NodeBalancerBackendIPv4Range is set, create the NodeBalancer in the specified VPC
47-
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" && clusterScope.LinodeCluster.Spec.VPCRef != nil {
129+
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range != "" && (clusterScope.LinodeCluster.Spec.VPCRef != nil || clusterScope.LinodeCluster.Spec.VPCID != nil) {
48130
logger.Info("Creating NodeBalancer in VPC", "NodeBalancerBackendIPv4Range", clusterScope.LinodeCluster.Spec.Network.NodeBalancerBackendIPv4Range)
49131
subnetID, err := getSubnetID(ctx, clusterScope, logger)
50132
if err != nil {
@@ -88,13 +170,43 @@ func EnsureNodeBalancer(ctx context.Context, clusterScope *scope.ClusterScope, l
88170
// getSubnetID returns the subnetID of the first subnet in the LinodeVPC.
89171
// If no subnets or subnetID is found, it returns an error.
90172
func getSubnetID(ctx context.Context, clusterScope *scope.ClusterScope, logger logr.Logger) (int, error) {
173+
subnetName := clusterScope.LinodeCluster.Spec.Network.SubnetName
174+
175+
// If direct VPCID is specified, get the VPC and subnets directly from Linode API
176+
if clusterScope.LinodeCluster.Spec.VPCID != nil {
177+
vpcID := *clusterScope.LinodeCluster.Spec.VPCID
178+
vpc, err := clusterScope.LinodeClient.GetVPC(ctx, vpcID)
179+
if err != nil {
180+
logger.Error(err, "Failed to fetch VPC from Linode API", "vpcID", vpcID)
181+
return 0, err
182+
}
183+
184+
if len(vpc.Subnets) == 0 {
185+
return 0, errors.New("no subnets found in VPC")
186+
}
187+
188+
subnetID, err := FindSubnet(subnetName, true, vpc.Subnets)
189+
if err != nil {
190+
logger.Error(err, "Failed to find subnet in VPC", "vpcID", vpcID, "subnetName", subnetName)
191+
return 0, err
192+
}
193+
return subnetID, nil
194+
}
195+
196+
// Otherwise, use the VPCRef
197+
if clusterScope.LinodeCluster.Spec.VPCRef == nil {
198+
return 0, errors.New("neither VPCID nor VPCRef is specified in LinodeCluster")
199+
}
200+
91201
name := clusterScope.LinodeCluster.Spec.VPCRef.Name
92202
namespace := clusterScope.LinodeCluster.Spec.VPCRef.Namespace
93203
if namespace == "" {
94204
namespace = clusterScope.LinodeCluster.Namespace
95205
}
96206

97-
logger = logger.WithValues("vpcName", name, "vpcNamespace", namespace)
207+
if name == "" {
208+
return 0, errors.New("VPCRef name is not specified in LinodeCluster")
209+
}
98210

99211
linodeVPC := &v1alpha2.LinodeVPC{
100212
ObjectMeta: metav1.ObjectMeta{
@@ -104,41 +216,16 @@ func getSubnetID(ctx context.Context, clusterScope *scope.ClusterScope, logger l
104216
}
105217

106218
objectKey := client.ObjectKeyFromObject(linodeVPC)
107-
err := clusterScope.Client.Get(ctx, objectKey, linodeVPC)
108-
if err != nil {
109-
logger.Error(err, "Failed to fetch LinodeVPC")
110-
return 0, err
219+
if err := clusterScope.Client.Get(ctx, objectKey, linodeVPC); err != nil {
220+
logger.Error(err, "Failed to fetch LinodeVPC", "name", name, "namespace", namespace)
221+
return 0, fmt.Errorf("failed to fetch LinodeVPC %s/%s: %w", namespace, name, err)
111222
}
112-
if len(linodeVPC.Spec.Subnets) == 0 {
113-
err = errors.New("No subnets found in LinodeVPC")
114-
logger.Error(err, "Failed to fetch LinodeVPC")
115-
return 0, err
116-
}
117-
118-
subnetID := 0
119-
subnetName := clusterScope.LinodeCluster.Spec.Network.SubnetName
120223

121-
// If subnet name specified, find matching subnet; otherwise use first subnet
122-
if subnetName != "" {
123-
for _, subnet := range linodeVPC.Spec.Subnets {
124-
if subnet.Label == subnetName {
125-
subnetID = subnet.SubnetID
126-
break
127-
}
128-
}
129-
if subnetID == 0 {
130-
return 0, fmt.Errorf("subnet with label %s not found in VPC", subnetName)
131-
}
132-
} else {
133-
subnetID = linodeVPC.Spec.Subnets[0].SubnetID
134-
}
135-
136-
// Validate the selected subnet ID
137-
if subnetID == 0 {
138-
return 0, errors.New("selected subnet ID is 0")
224+
if len(linodeVPC.Spec.Subnets) == 0 {
225+
return 0, errors.New("no subnets found in LinodeVPC")
139226
}
140227

141-
return subnetID, nil
228+
return FindSubnet(subnetName, false, linodeVPC.Spec.Subnets)
142229
}
143230

144231
func getFirewallID(ctx context.Context, clusterScope *scope.ClusterScope, logger logr.Logger) (int, error) {

0 commit comments

Comments
 (0)