Skip to content

Commit 1a10bbb

Browse files
authored
add vpc support for capl clusters (#159)
* add vpc support for capl clusters * disable kube-proxy, address review comments * use updated version of linode-ccm * address review comments * don't use /etc/hosts for node-ip * add additional interface using machinetemplate * rebase fix and address comments * reduce cognitive complexity, update tests and use ccm v0.4.3 for updated helm chart * update linode-ccm to v0.4.4 * address review comments, add vpc note --------- Co-authored-by: Rahul Sharma <[email protected]>
1 parent e518023 commit 1a10bbb

20 files changed

+254
-34
lines changed

controller/linodemachine_controller.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
ConditionPreflightAdditionalDisksCreated clusterv1.ConditionType = "PreflightAdditionalDisksCreated"
6262
ConditionPreflightConfigured clusterv1.ConditionType = "PreflightConfigured"
6363
ConditionPreflightBootTriggered clusterv1.ConditionType = "PreflightBootTriggered"
64+
ConditionPreflightNetworking clusterv1.ConditionType = "PreflightNetworking"
6465
ConditionPreflightReady clusterv1.ConditionType = "PreflightReady"
6566
)
6667

@@ -248,7 +249,6 @@ func (r *LinodeMachineReconciler) reconcile(
248249
return
249250
}
250251

251-
//nolint:cyclop // keep top-level preflight condition checks in the same function for readability
252252
func (r *LinodeMachineReconciler) reconcileCreate(
253253
ctx context.Context,
254254
logger logr.Logger,
@@ -312,8 +312,17 @@ func (r *LinodeMachineReconciler) reconcileCreate(
312312
return ctrl.Result{}, err
313313
}
314314

315+
return r.reconcileInstanceCreate(ctx, logger, machineScope, linodeInstance)
316+
}
317+
318+
func (r *LinodeMachineReconciler) reconcileInstanceCreate(
319+
ctx context.Context,
320+
logger logr.Logger,
321+
machineScope *scope.MachineScope,
322+
linodeInstance *linodego.Instance,
323+
) (ctrl.Result, error) {
315324
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightConfigured) {
316-
if err = r.configureDisks(ctx, logger, machineScope, linodeInstance.ID); err != nil {
325+
if err := r.configureDisks(ctx, logger, machineScope, linodeInstance.ID); err != nil {
317326
if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
318327
ConditionPreflightConfigured, string(cerrs.CreateMachineError), err.Error(),
319328
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
@@ -327,7 +336,7 @@ func (r *LinodeMachineReconciler) reconcileCreate(
327336
}
328337

329338
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightBootTriggered) {
330-
if err = machineScope.LinodeClient.BootInstance(ctx, linodeInstance.ID, 0); err != nil {
339+
if err := machineScope.LinodeClient.BootInstance(ctx, linodeInstance.ID, 0); err != nil {
331340
logger.Error(err, "Failed to boot instance")
332341

333342
if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
@@ -342,10 +351,27 @@ func (r *LinodeMachineReconciler) reconcileCreate(
342351
conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightBootTriggered)
343352
}
344353

345-
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightReady) {
346-
if err = services.AddNodeToNB(ctx, logger, machineScope); err != nil {
354+
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightNetworking) {
355+
if err := services.AddNodeToNB(ctx, logger, machineScope); err != nil {
347356
logger.Error(err, "Failed to add instance to Node Balancer backend")
348357

358+
if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
359+
ConditionPreflightNetworking, string(cerrs.CreateMachineError), err.Error(),
360+
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
361+
return ctrl.Result{}, err
362+
}
363+
364+
return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
365+
}
366+
367+
conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightNetworking)
368+
}
369+
370+
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightReady) {
371+
addrs, err := r.buildInstanceAddrs(ctx, machineScope, linodeInstance.ID)
372+
if err != nil {
373+
logger.Error(err, "Failed to get instance ip addresses")
374+
349375
if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
350376
ConditionPreflightReady, string(cerrs.CreateMachineError), err.Error(),
351377
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
@@ -354,12 +380,12 @@ func (r *LinodeMachineReconciler) reconcileCreate(
354380

355381
return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
356382
}
383+
machineScope.LinodeMachine.Status.Addresses = addrs
357384

358385
conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightReady)
359386
}
360387

361388
machineScope.LinodeMachine.Spec.ProviderID = util.Pointer(fmt.Sprintf("linode://%d", linodeInstance.ID))
362-
machineScope.LinodeMachine.Status.Addresses = buildInstanceAddrs(linodeInstance)
363389

364390
// Set the instance state to signal preflight process is done
365391
machineScope.LinodeMachine.Status.InstanceState = util.Pointer(linodego.InstanceOffline)

controller/linodemachine_controller_helpers.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func (r *LinodeMachineReconciler) newCreateConfig(ctx context.Context, machineSc
8888
createConfig.RootPass = uuid.NewString()
8989
}
9090

91+
// if vpc, attach additional interface to linode (eth1)
9192
if machineScope.LinodeCluster.Spec.VPCRef != nil {
9293
iface, err := r.getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger)
9394
if err != nil {
@@ -101,20 +102,39 @@ func (r *LinodeMachineReconciler) newCreateConfig(ctx context.Context, machineSc
101102
return createConfig, nil
102103
}
103104

104-
func buildInstanceAddrs(linodeInstance *linodego.Instance) []clusterv1.MachineAddress {
105-
addrs := []clusterv1.MachineAddress{}
106-
for _, addr := range linodeInstance.IPv4 {
107-
addrType := clusterv1.MachineExternalIP
108-
if addr.IsPrivate() {
109-
addrType = clusterv1.MachineInternalIP
105+
func (r *LinodeMachineReconciler) buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, instanceID int) ([]clusterv1.MachineAddress, error) {
106+
addresses, err := machineScope.LinodeClient.GetInstanceIPAddresses(ctx, instanceID)
107+
if err != nil {
108+
return nil, fmt.Errorf("get instance ips: %w", err)
109+
}
110+
111+
// get the default instance config
112+
configs, err := machineScope.LinodeClient.ListInstanceConfigs(ctx, instanceID, &linodego.ListOptions{})
113+
if err != nil || len(configs) == 0 {
114+
return nil, fmt.Errorf("list instance configs: %w", err)
115+
}
116+
117+
ips := []clusterv1.MachineAddress{}
118+
// check if a node has public ip and store it
119+
if len(addresses.IPv4.Public) != 0 {
120+
ips = append(ips, clusterv1.MachineAddress{Address: addresses.IPv4.Public[0].Address, Type: clusterv1.MachineExternalIP})
121+
}
122+
123+
// Iterate over interfaces in config and find VPC specific ips
124+
for _, iface := range configs[0].Interfaces {
125+
if iface.VPCID != nil && iface.IPv4.VPC != "" {
126+
ips = append(ips, clusterv1.MachineAddress{Address: iface.IPv4.VPC, Type: clusterv1.MachineInternalIP})
110127
}
111-
addrs = append(addrs, clusterv1.MachineAddress{
112-
Type: addrType,
113-
Address: addr.String(),
114-
})
115128
}
116129

117-
return addrs
130+
// if a node has private ip, store it as well
131+
// NOTE: We specifically store VPC ips first so that they are used first during
132+
// bootstrap when we set `registrationMethod: internal-only-ips`
133+
if len(addresses.IPv4.Private) != 0 {
134+
ips = append(ips, clusterv1.MachineAddress{Address: addresses.IPv4.Private[0].Address, Type: clusterv1.MachineInternalIP})
135+
}
136+
137+
return ips, nil
118138
}
119139

120140
func (r *LinodeMachineReconciler) getOwnerMachine(ctx context.Context, linodeMachine infrav1alpha1.LinodeMachine, log logr.Logger) (*clusterv1.Machine, error) {

controller/linodemachine_controller_test.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,26 @@ var _ = Describe("create", Label("machine", "create"), func() {
148148
IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))},
149149
Status: linodego.InstanceOffline,
150150
}, nil)
151-
mockLinodeClient.EXPECT().
151+
bootInst := mockLinodeClient.EXPECT().
152152
BootInstance(ctx, 123, 0).
153153
After(createInst).
154154
Return(nil)
155+
getAddrs := mockLinodeClient.EXPECT().
156+
GetInstanceIPAddresses(ctx, 123).
157+
After(bootInst).
158+
Return(&linodego.InstanceIPAddressResponse{
159+
IPv4: &linodego.InstanceIPv4Response{
160+
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
161+
},
162+
}, nil)
163+
mockLinodeClient.EXPECT().
164+
ListInstanceConfigs(ctx, 123, gomock.Any()).
165+
After(getAddrs).
166+
Return([]linodego.InstanceConfig{{
167+
Devices: &linodego.InstanceConfigDeviceMap{
168+
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
169+
},
170+
}}, nil)
155171

156172
mScope := scope.MachineScope{
157173
Client: k8sClient,
@@ -308,14 +324,30 @@ var _ = Describe("create", Label("machine", "create"), func() {
308324
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
309325
},
310326
}, nil)
311-
mockLinodeClient.EXPECT().
327+
createNB := mockLinodeClient.EXPECT().
312328
CreateNodeBalancerNode(ctx, 1, 2, linodego.NodeBalancerNodeCreateOptions{
313329
Label: "mock",
314330
Address: "192.168.0.2:6443",
315331
Mode: linodego.ModeAccept,
316332
}).
317333
After(getAddrs).
318334
Return(nil, nil)
335+
getAddrs = mockLinodeClient.EXPECT().
336+
GetInstanceIPAddresses(ctx, 123).
337+
After(createNB).
338+
Return(&linodego.InstanceIPAddressResponse{
339+
IPv4: &linodego.InstanceIPv4Response{
340+
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
341+
},
342+
}, nil)
343+
mockLinodeClient.EXPECT().
344+
ListInstanceConfigs(ctx, 123, gomock.Any()).
345+
After(getAddrs).
346+
Return([]linodego.InstanceConfig{{
347+
Devices: &linodego.InstanceConfigDeviceMap{
348+
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
349+
},
350+
}}, nil)
319351

320352
mScope := scope.MachineScope{
321353
Client: k8sClient,
@@ -461,16 +493,38 @@ var _ = Describe("create", Label("machine", "create"), func() {
461493
Return(&linodego.InstanceIPAddressResponse{
462494
IPv4: &linodego.InstanceIPv4Response{
463495
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
496+
Public: []*linodego.InstanceIP{{Address: "172.0.0.2"}},
464497
},
465498
}, nil)
466-
mockLinodeClient.EXPECT().
499+
createNB := mockLinodeClient.EXPECT().
467500
CreateNodeBalancerNode(ctx, 1, 2, linodego.NodeBalancerNodeCreateOptions{
468501
Label: "mock",
469502
Address: "192.168.0.2:6443",
470503
Mode: linodego.ModeAccept,
471504
}).
472505
After(getAddrs).
473506
Return(nil, nil)
507+
getAddrs = mockLinodeClient.EXPECT().
508+
GetInstanceIPAddresses(ctx, 123).
509+
After(createNB).
510+
Return(&linodego.InstanceIPAddressResponse{
511+
IPv4: &linodego.InstanceIPv4Response{
512+
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
513+
Public: []*linodego.InstanceIP{{Address: "172.0.0.2"}},
514+
},
515+
}, nil)
516+
mockLinodeClient.EXPECT().
517+
ListInstanceConfigs(ctx, 123, gomock.Any()).
518+
After(getAddrs).
519+
Return([]linodego.InstanceConfig{{
520+
Devices: &linodego.InstanceConfigDeviceMap{
521+
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
522+
},
523+
Interfaces: []linodego.InstanceConfigInterface{{
524+
VPCID: ptr.To(1),
525+
IPv4: &linodego.VPCIPv4{VPC: "10.0.0.2"},
526+
}},
527+
}}, nil)
474528

475529
_, err = reconciler.reconcileCreate(ctx, logger, &mScope)
476530
Expect(err).NotTo(HaveOccurred())
@@ -483,10 +537,20 @@ var _ = Describe("create", Label("machine", "create"), func() {
483537
Expect(*linodeMachine.Status.InstanceState).To(Equal(linodego.InstanceOffline))
484538
Expect(*linodeMachine.Spec.InstanceID).To(Equal(123))
485539
Expect(*linodeMachine.Spec.ProviderID).To(Equal("linode://123"))
486-
Expect(linodeMachine.Status.Addresses).To(Equal([]clusterv1.MachineAddress{{
487-
Type: clusterv1.MachineInternalIP,
488-
Address: "192.168.0.2",
489-
}}))
540+
Expect(linodeMachine.Status.Addresses).To(Equal([]clusterv1.MachineAddress{
541+
{
542+
Type: clusterv1.MachineExternalIP,
543+
Address: "172.0.0.2",
544+
},
545+
{
546+
Type: clusterv1.MachineInternalIP,
547+
Address: "10.0.0.2",
548+
},
549+
{
550+
Type: clusterv1.MachineInternalIP,
551+
Address: "192.168.0.2",
552+
},
553+
}))
490554

491555
Expect(testLogs.String()).To(ContainSubstring("creating machine"))
492556
Expect(testLogs.String()).To(ContainSubstring("Linode instance already exists"))

docs/src/developers/development.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ clusterctl generate cluster $CLUSTER_NAME \
213213
| kubectl apply -f -
214214
```
215215

216-
This will provision the cluster with the CNI defaulted to [cilium](../topics/addons.md#cilium)
216+
This will provision the cluster within VPC with the CNI defaulted to [cilium](../topics/addons.md#cilium)
217217
and the [linode-ccm](../topics/addons.md#ccm) installed.
218218

219219
##### Using ClusterClass (alpha)
@@ -245,6 +245,9 @@ To delete the cluster, simply run:
245245
```sh
246246
kubectl delete cluster $CLUSTER_NAME
247247
```
248+
```admonish warning
249+
VPCs are not deleted when a cluster is deleted using kubectl. One can run `kubectl delete linodevpc <vpcname>` to cleanup VPC once cluster is deleted.
250+
```
248251

249252
```admonish question title=""
250253
For any issues, please refer to the [troubleshooting guide](../topics/troubleshooting.md).

docs/src/topics/getting-started.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export LINODE_MACHINE_TYPE=g6-standard-2
3232
For Regions and Images that do not yet support Akamai's cloud-init datasource CAPL will automatically use a stackscript shim
3333
to provision the node. If you are using a custom image ensure the [cloud_init](https://www.linode.com/docs/api/images/#image-create) flag is set correctly on it
3434
```
35+
```admonish warning
36+
By default, clusters are provisioned within VPC. For Regions which do not have [VPC support](https://www.linode.com/docs/products/networking/vpc/#availability) yet, use the VPCLess[TODO] flavor to have clusters provisioned.
37+
```
3538

3639
## Register linode as an infrastructure provider
3740
1. Add `linode` as an infrastructure provider in `~/.cluster-api/clusterctl.yaml`

templates/addons/cilium/cilium.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,31 @@ spec:
1818
valuesTemplate: |
1919
bgpControlPlane:
2020
enabled: true
21+
routingMode: native
22+
kubeProxyReplacement: true
23+
ipv4NativeRoutingCIDR: 10.0.0.0/8
24+
tunnelProtocol: ""
25+
enableIPv4Masquerade: true
26+
egressMasqueradeInterfaces: eth0
27+
k8sServiceHost: {{ .InfraCluster.spec.controlPlaneEndpoint.host }}
28+
k8sServicePort: {{ .InfraCluster.spec.controlPlaneEndpoint.port }}
29+
extraArgs:
30+
- --direct-routing-device=eth1
31+
- --nodeport-addresses=0.0.0.0/0
2132
ipam:
2233
mode: kubernetes
34+
ipv4:
35+
enabled: true
36+
ipv6:
37+
enabled: false
2338
k8s:
2439
requireIPv4PodCIDR: true
2540
hubble:
2641
relay:
2742
enabled: true
2843
ui:
2944
enabled: true
45+
# ipMasqAgent:
46+
# enabled: true
47+
# bpf:
48+
# masquerade: true

templates/addons/provider-linode/linode-ccm.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ spec:
99
repoURL: https://linode.github.io/linode-cloud-controller-manager/
1010
chartName: ccm-linode
1111
namespace: kube-system
12-
version: ${LINODE_CCM_VERSION:=v0.3.24}
12+
version: ${LINODE_CCM_VERSION:=v0.4.4}
1313
options:
1414
waitForJobs: true
1515
wait: true
1616
timeout: 5m
1717
valuesTemplate: |
18+
routeController:
19+
vpcName: ${VPC_NAME:=${CLUSTER_NAME}}
20+
clusterCIDR: 10.0.0.0/8
21+
configureCloudRoutes: true
1822
secretRef:
1923
name: "linode-token-region"
2024
image:

templates/common-init-files/secret.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ stringData:
3838
modprobe overlay
3939
modprobe br_netfilter
4040
sysctl --system
41-
41+
IPADDR=$(ip a s eth1 |grep 'inet ' |cut -d' ' -f6|cut -d/ -f1)
42+
sed -i "s/kubeletExtraArgs:/kubeletExtraArgs:\n node-ip: $IPADDR/g" /run/kubeadm/kubeadm.yaml

templates/flavors/base/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
22
kind: Kustomization
33
resources:
44
- cluster.yaml
5+
- linodeVPC.yaml
56
- linodeCluster.yaml
67
- linodeMachineTemplate.yaml
78
- machineDeployment.yaml

templates/flavors/base/linodeCluster.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ spec:
77
region: ${LINODE_REGION}
88
credentialsRef:
99
name: ${CLUSTER_NAME}-credentials
10+
vpcRef:
11+
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
12+
kind: LinodeVPC
13+
name: ${VPC_NAME:=${CLUSTER_NAME}}

templates/flavors/base/linodeMachineTemplate.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ spec:
99
image: ${LINODE_OS:="linode/ubuntu22.04"}
1010
type: ${LINODE_CONTROL_PLANE_MACHINE_TYPE}
1111
region: ${LINODE_REGION}
12+
interfaces:
13+
- purpose: public
14+
primary: true
1215
authorizedKeys:
1316
# uncomment to include your ssh key in linode provisioning
1417
# - ${LINODE_SSH_PUBKEY:=""}
@@ -23,6 +26,9 @@ spec:
2326
image: ${LINODE_OS:="linode/ubuntu22.04"}
2427
type: ${LINODE_MACHINE_TYPE}
2528
region: ${LINODE_REGION}
29+
interfaces:
30+
- purpose: public
31+
primary: true
2632
authorizedKeys:
2733
# uncomment to include your ssh key in linode provisioning
2834
# - ${LINODE_SSH_PUBKEY:=""}

0 commit comments

Comments
 (0)