From d511370156ae81986022d84731a5641250eb74ad Mon Sep 17 00:00:00 2001 From: Evan Johnson Date: Wed, 18 Sep 2024 17:11:55 -0400 Subject: [PATCH] add support for configuring the VPC interface --- .markdownlinkcheck.json | 2 + .../linodemachine_controller_helpers.go | 17 +- controller/linodemachine_controller_test.go | 268 ++++++++++++++++++ docs/src/topics/vpc.md | 26 ++ 4 files changed, 308 insertions(+), 5 deletions(-) diff --git a/.markdownlinkcheck.json b/.markdownlinkcheck.json index 9c14d740b..8687e5284 100644 --- a/.markdownlinkcheck.json +++ b/.markdownlinkcheck.json @@ -1,6 +1,8 @@ { "ignorePatterns": [ { "pattern": "^https://www.linode.com" }, + { "pattern": "^https://www.techdocs.akamai.com" }, + { "pattern": "^https://techdocs.akamai.com" }, { "pattern": "^https://linode.com" }, { "pattern": "^http://localhost" } ], diff --git a/controller/linodemachine_controller_helpers.go b/controller/linodemachine_controller_helpers.go index 5cee4c491..d73321d48 100644 --- a/controller/linodemachine_controller_helpers.go +++ b/controller/linodemachine_controller_helpers.go @@ -111,15 +111,16 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg // if vpc, attach additional interface as eth0 to linode if machineScope.LinodeCluster.Spec.VPCRef != nil { - iface, err := getVPCInterfaceConfig(ctx, machineScope, logger) + iface, err := getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger) if err != nil { logger.Error(err, "Failed to get VPC interface config") return nil, err } - - // add VPC interface as first interface - createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface) + if iface != nil { + // add VPC interface as first interface + createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface) + } } if machineScope.LinodeMachine.Spec.PlacementGroupRef != nil { @@ -333,7 +334,7 @@ func getFirewallID(ctx context.Context, machineScope *scope.MachineScope, logger return *linodeFirewall.Spec.FirewallID, nil } -func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceConfigInterfaceCreateOptions, error) { +func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger) (*linodego.InstanceConfigInterfaceCreateOptions, error) { name := machineScope.LinodeCluster.Spec.VPCRef.Name namespace := machineScope.LinodeCluster.Spec.VPCRef.Namespace if namespace == "" { @@ -381,6 +382,12 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope }) subnetID = vpc.Subnets[0].ID + for i, netInterface := range interfaces { + if netInterface.Purpose == linodego.InterfacePurposeVPC { + interfaces[i].SubnetID = &subnetID + return nil, nil //nolint:nilnil // it is important we don't return an interface if a VPC interface already exists + } + } return &linodego.InstanceConfigInterfaceCreateOptions{ Purpose: linodego.InterfacePurposeVPC, diff --git a/controller/linodemachine_controller_test.go b/controller/linodemachine_controller_test.go index bc2011212..13998d1bd 100644 --- a/controller/linodemachine_controller_test.go +++ b/controller/linodemachine_controller_test.go @@ -1566,3 +1566,271 @@ var _ = Describe("machine in PlacementGroup", Label("machine", "placementGroup") Expect(createOpts.FirewallID).To(Equal(2)) }) }) + +var _ = Describe("machine in VPC", Label("machine", "VPC"), Ordered, func() { + var machine clusterv1.Machine + var secret corev1.Secret + var lvpcReconciler *LinodeVPCReconciler + var linodeVPC infrav1alpha2.LinodeVPC + + var mockCtrl *gomock.Controller + var testLogs *bytes.Buffer + var logger logr.Logger + + cluster := clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mock", + Namespace: defaultNamespace, + }, + } + + linodeCluster := infrav1alpha2.LinodeCluster{ + Spec: infrav1alpha2.LinodeClusterSpec{ + Region: "us-ord", + Network: infrav1alpha2.NetworkSpec{ + LoadBalancerType: "dns", + DNSRootDomain: "lkedevs.net", + DNSUniqueIdentifier: "abc123", + DNSTTLSec: 30, + }, + VPCRef: &corev1.ObjectReference{ + Namespace: "default", + Kind: "LinodeVPC", + Name: "test-cluster", + }, + }, + } + + recorder := record.NewFakeRecorder(10) + + BeforeEach(func(ctx SpecContext) { + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-secret", + Namespace: defaultNamespace, + }, + Data: map[string][]byte{ + "value": []byte("userdata"), + }, + } + Expect(k8sClient.Create(ctx, &secret)).To(Succeed()) + + machine = clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: defaultNamespace, + Labels: make(map[string]string), + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + DataSecretName: ptr.To("bootstrap-secret"), + }, + }, + } + + linodeVPC = infrav1alpha2.LinodeVPC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: defaultNamespace, + UID: "5123122", + }, + Spec: infrav1alpha2.LinodeVPCSpec{ + VPCID: ptr.To(1), + Region: "us-ord", + Subnets: []infrav1alpha2.VPCSubnetCreateOptions{}, + }, + Status: infrav1alpha2.LinodeVPCStatus{ + Ready: true, + }, + } + Expect(k8sClient.Create(ctx, &linodeVPC)).To(Succeed()) + + lvpcReconciler = &LinodeVPCReconciler{ + Recorder: recorder, + Client: k8sClient, + } + + mockCtrl = gomock.NewController(GinkgoT()) + testLogs = &bytes.Buffer{} + logger = zap.New( + zap.WriteTo(GinkgoWriter), + zap.WriteTo(testLogs), + zap.UseDevMode(true), + ) + }) + + AfterEach(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, &secret)).To(Succeed()) + var currentVPC infrav1alpha2.LinodeVPC + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&linodeVPC), ¤tVPC)).To(Succeed()) + currentVPC.Finalizers = nil + + Expect(k8sClient.Update(ctx, ¤tVPC)).To(Succeed()) + + Expect(k8sClient.Delete(ctx, ¤tVPC)).To(Succeed()) + + mockCtrl.Finish() + for len(recorder.Events) > 0 { + <-recorder.Events + } + }) + + It("creates a instance with vpc", func(ctx SpecContext) { + linodeMachine := infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mock", + Namespace: defaultNamespace, + UID: "12345", + }, + Spec: infrav1alpha2.LinodeMachineSpec{ + ProviderID: ptr.To("linode://0"), + Type: "g6-nanode-1", + Interfaces: []infrav1alpha2.InstanceConfigInterfaceCreateOptions{ + { + Primary: true, + }, + }, + }, + } + mockLinodeClient := mock.NewMockLinodeClient(mockCtrl) + getRegion := mockLinodeClient.EXPECT(). + GetRegion(ctx, gomock.Any()). + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, infrav1alpha2.LinodePlacementGroupCapability}}, nil) + mockLinodeClient.EXPECT(). + GetImage(ctx, gomock.Any()). + After(getRegion). + Return(&linodego.Image{Capabilities: []string{"cloud-init"}}, nil) + mockLinodeClient.EXPECT(). + ListVPCs(ctx, gomock.Any()). + Return([]linodego.VPC{}, nil) + mockLinodeClient.EXPECT(). + CreateVPC(ctx, gomock.Any()). + Return(&linodego.VPC{ID: 1}, nil) + mockLinodeClient.EXPECT(). + GetVPC(ctx, gomock.Any()). + Return(&linodego.VPC{ID: 1, Subnets: []linodego.VPCSubnet{{ + ID: 1, + Label: "test", + IPv4: "10.0.0.0/24", + }}}, nil) + helper, err := patch.NewHelper(&linodeVPC, k8sClient) + Expect(err).NotTo(HaveOccurred()) + + _, err = lvpcReconciler.reconcile(ctx, logger, &scope.VPCScope{ + PatchHelper: helper, + Client: k8sClient, + LinodeClient: mockLinodeClient, + LinodeVPC: &linodeVPC, + }) + + Expect(err).NotTo(HaveOccurred()) + + mScope := scope.MachineScope{ + Client: k8sClient, + LinodeClient: mockLinodeClient, + Cluster: &cluster, + Machine: &machine, + LinodeCluster: &linodeCluster, + LinodeMachine: &linodeMachine, + } + + patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient) + Expect(err).NotTo(HaveOccurred()) + mScope.PatchHelper = patchHelper + + createOpts, err := newCreateConfig(ctx, &mScope, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(createOpts).NotTo(BeNil()) + Expect(createOpts.Interfaces).To(Equal([]linodego.InstanceConfigInterfaceCreateOptions{ + { + Purpose: linodego.InterfacePurposeVPC, + Primary: true, + SubnetID: ptr.To(1), + IPv4: &linodego.VPCIPv4{NAT1To1: ptr.To("any")}, + }, + { + Primary: true, + }})) + }) + It("creates a instance with pre defined vpc interface", func(ctx SpecContext) { + linodeMachine := infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mock", + Namespace: defaultNamespace, + UID: "12345", + }, + Spec: infrav1alpha2.LinodeMachineSpec{ + ProviderID: ptr.To("linode://0"), + Type: "g6-nanode-1", + Interfaces: []infrav1alpha2.InstanceConfigInterfaceCreateOptions{ + { + Purpose: linodego.InterfacePurposeVPC, + Primary: false, + }, + { + Purpose: linodego.InterfacePurposePublic, + Primary: true, + }, + }, + }, + } + mockLinodeClient := mock.NewMockLinodeClient(mockCtrl) + getRegion := mockLinodeClient.EXPECT(). + GetRegion(ctx, gomock.Any()). + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, infrav1alpha2.LinodePlacementGroupCapability}}, nil) + mockLinodeClient.EXPECT(). + GetImage(ctx, gomock.Any()). + After(getRegion). + Return(&linodego.Image{Capabilities: []string{"cloud-init"}}, nil) + mockLinodeClient.EXPECT(). + ListVPCs(ctx, gomock.Any()). + Return([]linodego.VPC{}, nil) + mockLinodeClient.EXPECT(). + CreateVPC(ctx, gomock.Any()). + Return(&linodego.VPC{ID: 1}, nil) + mockLinodeClient.EXPECT(). + GetVPC(ctx, gomock.Any()). + Return(&linodego.VPC{ID: 1, Subnets: []linodego.VPCSubnet{{ + ID: 1, + Label: "test", + IPv4: "10.0.0.0/24", + }}}, nil) + helper, err := patch.NewHelper(&linodeVPC, k8sClient) + Expect(err).NotTo(HaveOccurred()) + + _, err = lvpcReconciler.reconcile(ctx, logger, &scope.VPCScope{ + PatchHelper: helper, + Client: k8sClient, + LinodeClient: mockLinodeClient, + LinodeVPC: &linodeVPC, + }) + + Expect(err).NotTo(HaveOccurred()) + + mScope := scope.MachineScope{ + Client: k8sClient, + LinodeClient: mockLinodeClient, + Cluster: &cluster, + Machine: &machine, + LinodeCluster: &linodeCluster, + LinodeMachine: &linodeMachine, + } + + patchHelper, err := patch.NewHelper(mScope.LinodeMachine, k8sClient) + Expect(err).NotTo(HaveOccurred()) + mScope.PatchHelper = patchHelper + + createOpts, err := newCreateConfig(ctx, &mScope, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(createOpts).NotTo(BeNil()) + Expect(createOpts.Interfaces).To(Equal([]linodego.InstanceConfigInterfaceCreateOptions{ + { + Purpose: linodego.InterfacePurposeVPC, + Primary: false, + SubnetID: ptr.To(1), + }, + { + Purpose: linodego.InterfacePurposePublic, + Primary: true, + }})) + }) +}) diff --git a/docs/src/topics/vpc.md b/docs/src/topics/vpc.md index 119952c29..f0b368e07 100644 --- a/docs/src/topics/vpc.md +++ b/docs/src/topics/vpc.md @@ -15,6 +15,32 @@ Key facts about VPC network configuration: 4. [Kubernetes host-scope IPAM mode](https://docs.cilium.io/en/stable/network/concepts/ipam/kubernetes/) is used to assign pod CIDRs to nodes. We run [linode CCM](https://github.com/linode/linode-cloud-controller-manager) with [route-controller enabled](https://github.com/linode/linode-cloud-controller-manager?tab=readme-ov-file#routes) which automatically adds/updates routes within VPC when pod cidrs are added/updated by k8s. This enables pod-to-pod traffic to be routable within the VPC. 5. kube-proxy is disabled by default. + +## Configuring the VPC interface +In order to configure the VPC interface beyond the default above, an explicit interface can be configured in the `LinodeMachineTemplate`. +When the `LinodeMachine` controller find an interface with `purpose: vpc` it will automatically inject the `SubnetID` from the +`VPCRef`. + +_Example template where the VPC interface is not the primary interface_ +```yaml +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeMachineTemplate +metadata: + name: test-cluster-md-0 + namespace: default +spec: + template: + spec: + region: "us-mia" + type: "g6-standard-4" + image: linode/ubuntu22.04 + interfaces: + - purpose: vpc + primary: false + - purpose: public + primary: true +``` ## How VPC is provisioned A VPC is tied to a region. CAPL generates LinodeVPC manifest which contains the VPC name, region and subnet information. By defult, VPC name is set to cluster name but can be overwritten by specifying relevant environment variable.