Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add vpc support for capl clusters #159

Merged
merged 10 commits into from
Apr 25, 2024
38 changes: 32 additions & 6 deletions controller/linodemachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
ConditionPreflightAdditionalDisksCreated clusterv1.ConditionType = "PreflightAdditionalDisksCreated"
ConditionPreflightConfigured clusterv1.ConditionType = "PreflightConfigured"
ConditionPreflightBootTriggered clusterv1.ConditionType = "PreflightBootTriggered"
ConditionPreflightNetworking clusterv1.ConditionType = "PreflightNetworking"
ConditionPreflightReady clusterv1.ConditionType = "PreflightReady"
)

Expand Down Expand Up @@ -248,7 +249,6 @@
return
}

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

return r.reconcileInstanceCreate(ctx, logger, machineScope, linodeInstance)
}

func (r *LinodeMachineReconciler) reconcileInstanceCreate(
ctx context.Context,
logger logr.Logger,
machineScope *scope.MachineScope,
linodeInstance *linodego.Instance,
) (ctrl.Result, error) {
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightConfigured) {
if err = r.configureDisks(ctx, logger, machineScope, linodeInstance.ID); err != nil {
if err := r.configureDisks(ctx, logger, machineScope, linodeInstance.ID); err != nil {
if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
ConditionPreflightConfigured, string(cerrs.CreateMachineError), err.Error(),
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
Expand All @@ -327,7 +336,7 @@
}

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

if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
Expand All @@ -342,10 +351,27 @@
conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightBootTriggered)
}

if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightReady) {
if err = services.AddNodeToNB(ctx, logger, machineScope); err != nil {
if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightNetworking) {
if err := services.AddNodeToNB(ctx, logger, machineScope); err != nil {
logger.Error(err, "Failed to add instance to Node Balancer backend")

if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
ConditionPreflightNetworking, string(cerrs.CreateMachineError), err.Error(),
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
return ctrl.Result{}, err

Check warning on line 361 in controller/linodemachine_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller.go#L358-L361

Added lines #L358 - L361 were not covered by tests
}

return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil

Check warning on line 364 in controller/linodemachine_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller.go#L364

Added line #L364 was not covered by tests
}

conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightNetworking)
}

if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightReady) {
addrs, err := r.buildInstanceAddrs(ctx, machineScope, linodeInstance.ID)
if err != nil {
logger.Error(err, "Failed to get instance ip addresses")

Check warning on line 373 in controller/linodemachine_controller.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller.go#L373

Added line #L373 was not covered by tests

if reconciler.RecordDecayingCondition(machineScope.LinodeMachine,
ConditionPreflightReady, string(cerrs.CreateMachineError), err.Error(),
reconciler.DefaultMachineControllerPreflightTimeout(r.ReconcileTimeout)) {
Expand All @@ -354,12 +380,12 @@

return ctrl.Result{RequeueAfter: reconciler.DefaultMachineControllerWaitForRunningDelay}, nil
}
machineScope.LinodeMachine.Status.Addresses = addrs

conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightReady)
}

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

// Set the instance state to signal preflight process is done
machineScope.LinodeMachine.Status.InstanceState = util.Pointer(linodego.InstanceOffline)
Expand Down
42 changes: 31 additions & 11 deletions controller/linodemachine_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
createConfig.RootPass = uuid.NewString()
}

// if vpc, attach additional interface to linode (eth1)
if machineScope.LinodeCluster.Spec.VPCRef != nil {
iface, err := r.getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger)
if err != nil {
Expand All @@ -101,20 +102,39 @@
return createConfig, nil
}

func buildInstanceAddrs(linodeInstance *linodego.Instance) []clusterv1.MachineAddress {
addrs := []clusterv1.MachineAddress{}
for _, addr := range linodeInstance.IPv4 {
addrType := clusterv1.MachineExternalIP
if addr.IsPrivate() {
addrType = clusterv1.MachineInternalIP
func (r *LinodeMachineReconciler) buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, instanceID int) ([]clusterv1.MachineAddress, error) {
addresses, err := machineScope.LinodeClient.GetInstanceIPAddresses(ctx, instanceID)
bcm820 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("get instance ips: %w", err)

Check warning on line 108 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L108

Added line #L108 was not covered by tests
}

// get the default instance config
configs, err := machineScope.LinodeClient.ListInstanceConfigs(ctx, instanceID, &linodego.ListOptions{})
if err != nil || len(configs) == 0 {
return nil, fmt.Errorf("list instance configs: %w", err)

Check warning on line 114 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L114

Added line #L114 was not covered by tests
}

ips := []clusterv1.MachineAddress{}
// check if a node has public ip and store it
if len(addresses.IPv4.Public) != 0 {
ips = append(ips, clusterv1.MachineAddress{Address: addresses.IPv4.Public[0].Address, Type: clusterv1.MachineExternalIP})
}

// Iterate over interfaces in config and find VPC specific ips
for _, iface := range configs[0].Interfaces {
if iface.VPCID != nil && iface.IPv4.VPC != "" {
ips = append(ips, clusterv1.MachineAddress{Address: iface.IPv4.VPC, Type: clusterv1.MachineInternalIP})
}
addrs = append(addrs, clusterv1.MachineAddress{
Type: addrType,
Address: addr.String(),
})
}

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

return ips, nil
}

func (r *LinodeMachineReconciler) getOwnerMachine(ctx context.Context, linodeMachine infrav1alpha1.LinodeMachine, log logr.Logger) (*clusterv1.Machine, error) {
Expand Down
78 changes: 71 additions & 7 deletions controller/linodemachine_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,26 @@ var _ = Describe("create", Label("machine", "create"), func() {
IPv4: []*net.IP{ptr.To(net.IPv4(192, 168, 0, 2))},
Status: linodego.InstanceOffline,
}, nil)
mockLinodeClient.EXPECT().
bootInst := mockLinodeClient.EXPECT().
BootInstance(ctx, 123, 0).
After(createInst).
Return(nil)
getAddrs := mockLinodeClient.EXPECT().
GetInstanceIPAddresses(ctx, 123).
After(bootInst).
Return(&linodego.InstanceIPAddressResponse{
IPv4: &linodego.InstanceIPv4Response{
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
},
}, nil)
mockLinodeClient.EXPECT().
ListInstanceConfigs(ctx, 123, gomock.Any()).
After(getAddrs).
Return([]linodego.InstanceConfig{{
Devices: &linodego.InstanceConfigDeviceMap{
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
},
}}, nil)

mScope := scope.MachineScope{
Client: k8sClient,
Expand Down Expand Up @@ -308,14 +324,30 @@ var _ = Describe("create", Label("machine", "create"), func() {
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
},
}, nil)
mockLinodeClient.EXPECT().
createNB := mockLinodeClient.EXPECT().
CreateNodeBalancerNode(ctx, 1, 2, linodego.NodeBalancerNodeCreateOptions{
Label: "mock",
Address: "192.168.0.2:6443",
Mode: linodego.ModeAccept,
}).
After(getAddrs).
Return(nil, nil)
getAddrs = mockLinodeClient.EXPECT().
GetInstanceIPAddresses(ctx, 123).
After(createNB).
Return(&linodego.InstanceIPAddressResponse{
IPv4: &linodego.InstanceIPv4Response{
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
},
}, nil)
mockLinodeClient.EXPECT().
ListInstanceConfigs(ctx, 123, gomock.Any()).
After(getAddrs).
Return([]linodego.InstanceConfig{{
Devices: &linodego.InstanceConfigDeviceMap{
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
},
}}, nil)

mScope := scope.MachineScope{
Client: k8sClient,
Expand Down Expand Up @@ -461,16 +493,38 @@ var _ = Describe("create", Label("machine", "create"), func() {
Return(&linodego.InstanceIPAddressResponse{
IPv4: &linodego.InstanceIPv4Response{
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
Public: []*linodego.InstanceIP{{Address: "172.0.0.2"}},
},
}, nil)
mockLinodeClient.EXPECT().
createNB := mockLinodeClient.EXPECT().
CreateNodeBalancerNode(ctx, 1, 2, linodego.NodeBalancerNodeCreateOptions{
Label: "mock",
Address: "192.168.0.2:6443",
Mode: linodego.ModeAccept,
}).
After(getAddrs).
Return(nil, nil)
getAddrs = mockLinodeClient.EXPECT().
GetInstanceIPAddresses(ctx, 123).
After(createNB).
Return(&linodego.InstanceIPAddressResponse{
IPv4: &linodego.InstanceIPv4Response{
Private: []*linodego.InstanceIP{{Address: "192.168.0.2"}},
Public: []*linodego.InstanceIP{{Address: "172.0.0.2"}},
},
}, nil)
mockLinodeClient.EXPECT().
ListInstanceConfigs(ctx, 123, gomock.Any()).
After(getAddrs).
Return([]linodego.InstanceConfig{{
Devices: &linodego.InstanceConfigDeviceMap{
SDA: &linodego.InstanceConfigDevice{DiskID: 100},
},
Interfaces: []linodego.InstanceConfigInterface{{
VPCID: ptr.To(1),
IPv4: &linodego.VPCIPv4{VPC: "10.0.0.2"},
}},
}}, nil)

_, err = reconciler.reconcileCreate(ctx, logger, &mScope)
Expect(err).NotTo(HaveOccurred())
Expand All @@ -483,10 +537,20 @@ var _ = Describe("create", Label("machine", "create"), func() {
Expect(*linodeMachine.Status.InstanceState).To(Equal(linodego.InstanceOffline))
Expect(*linodeMachine.Spec.InstanceID).To(Equal(123))
Expect(*linodeMachine.Spec.ProviderID).To(Equal("linode://123"))
Expect(linodeMachine.Status.Addresses).To(Equal([]clusterv1.MachineAddress{{
Type: clusterv1.MachineInternalIP,
Address: "192.168.0.2",
}}))
Expect(linodeMachine.Status.Addresses).To(Equal([]clusterv1.MachineAddress{
{
Type: clusterv1.MachineExternalIP,
Address: "172.0.0.2",
},
{
Type: clusterv1.MachineInternalIP,
Address: "10.0.0.2",
},
{
Type: clusterv1.MachineInternalIP,
Address: "192.168.0.2",
},
}))

Expect(testLogs.String()).To(ContainSubstring("creating machine"))
Expect(testLogs.String()).To(ContainSubstring("Linode instance already exists"))
Expand Down
5 changes: 4 additions & 1 deletion docs/src/developers/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ clusterctl generate cluster $CLUSTER_NAME \
| kubectl apply -f -
```

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

##### Using ClusterClass (alpha)
Expand Down Expand Up @@ -245,6 +245,9 @@ To delete the cluster, simply run:
```sh
kubectl delete cluster $CLUSTER_NAME
```
```admonish warning
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.
```

```admonish question title=""
For any issues, please refer to the [troubleshooting guide](../topics/troubleshooting.md).
Expand Down
3 changes: 3 additions & 0 deletions docs/src/topics/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export LINODE_MACHINE_TYPE=g6-standard-2
For Regions and Images that do not yet support Akamai's cloud-init datasource CAPL will automatically use a stackscript shim
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
```
```admonish warning
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.
```

## Register linode as an infrastructure provider
1. Add `linode` as an infrastructure provider in `~/.cluster-api/clusterctl.yaml`
Expand Down
19 changes: 19 additions & 0 deletions templates/addons/cilium/cilium.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,31 @@ spec:
valuesTemplate: |
bgpControlPlane:
enabled: true
routingMode: native
kubeProxyReplacement: true
eljohnson92 marked this conversation as resolved.
Show resolved Hide resolved
ipv4NativeRoutingCIDR: 10.0.0.0/8
tunnelProtocol: ""
enableIPv4Masquerade: true
egressMasqueradeInterfaces: eth0
k8sServiceHost: {{ .InfraCluster.spec.controlPlaneEndpoint.host }}
k8sServicePort: {{ .InfraCluster.spec.controlPlaneEndpoint.port }}
extraArgs:
- --direct-routing-device=eth1
- --nodeport-addresses=0.0.0.0/0
ipam:
mode: kubernetes
ipv4:
enabled: true
ipv6:
eljohnson92 marked this conversation as resolved.
Show resolved Hide resolved
enabled: false
k8s:
requireIPv4PodCIDR: true
hubble:
relay:
enabled: true
ui:
enabled: true
# ipMasqAgent:
# enabled: true
# bpf:
# masquerade: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not using bpf by default as I want to get VPC working first. Once the PR is merged, we can have a minor PR to switch from iptables based masquerading to bpf based masquerading.

6 changes: 5 additions & 1 deletion templates/addons/provider-linode/linode-ccm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ spec:
repoURL: https://linode.github.io/linode-cloud-controller-manager/
chartName: ccm-linode
namespace: kube-system
version: ${LINODE_CCM_VERSION:=v0.3.24}
version: ${LINODE_CCM_VERSION:=v0.4.4}
options:
waitForJobs: true
wait: true
timeout: 5m
valuesTemplate: |
routeController:
vpcName: ${VPC_NAME:=${CLUSTER_NAME}}
clusterCIDR: 10.0.0.0/8
configureCloudRoutes: true
secretRef:
name: "linode-token-region"
image:
Expand Down
3 changes: 2 additions & 1 deletion templates/common-init-files/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ stringData:
modprobe overlay
modprobe br_netfilter
sysctl --system

IPADDR=$(ip a s eth1 |grep 'inet ' |cut -d' ' -f6|cut -d/ -f1)
sed -i "s/kubeletExtraArgs:/kubeletExtraArgs:\n node-ip: $IPADDR/g" /run/kubeadm/kubeadm.yaml
1 change: 1 addition & 0 deletions templates/flavors/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cluster.yaml
- linodeVPC.yaml
- linodeCluster.yaml
- linodeMachineTemplate.yaml
- machineDeployment.yaml
Expand Down
4 changes: 4 additions & 0 deletions templates/flavors/base/linodeCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ spec:
region: ${LINODE_REGION}
credentialsRef:
name: ${CLUSTER_NAME}-credentials
vpcRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: LinodeVPC
name: ${VPC_NAME:=${CLUSTER_NAME}}
6 changes: 6 additions & 0 deletions templates/flavors/base/linodeMachineTemplate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ spec:
image: ${LINODE_OS:="linode/ubuntu22.04"}
type: ${LINODE_CONTROL_PLANE_MACHINE_TYPE}
region: ${LINODE_REGION}
interfaces:
- purpose: public
primary: true
authorizedKeys:
# uncomment to include your ssh key in linode provisioning
# - ${LINODE_SSH_PUBKEY:=""}
rahulait marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -23,6 +26,9 @@ spec:
image: ${LINODE_OS:="linode/ubuntu22.04"}
type: ${LINODE_MACHINE_TYPE}
region: ${LINODE_REGION}
interfaces:
- purpose: public
primary: true
authorizedKeys:
# uncomment to include your ssh key in linode provisioning
# - ${LINODE_SSH_PUBKEY:=""}
rahulait marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading