Skip to content

Commit

Permalink
[feat] VLAN support for CAPL clusters (#525)
Browse files Browse the repository at this point in the history
Introduce VLAN support for CAPL clusters

---------

Co-authored-by: Rahul Sharma <rahulait@users.noreply.github.com>
  • Loading branch information
tchinmai7 and rahulait authored Oct 7, 2024
1 parent 8cb7af6 commit bb94b10
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 32 deletions.
5 changes: 5 additions & 0 deletions api/v1alpha1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,8 @@ func Convert_v1alpha2_LinodeObjectStorageBucket_To_v1alpha1_LinodeObjectStorageB
}
return autoConvert_v1alpha2_LinodeObjectStorageBucket_To_v1alpha1_LinodeObjectStorageBucket(in, out, scope)
}

func Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in *infrastructurev1alpha2.LinodeClusterSpec, out *LinodeClusterSpec, scope conversion.Scope) error {
// VLAN is not supported in v1alpha1
return autoConvert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in, out, scope)
}
16 changes: 6 additions & 10 deletions api/v1alpha1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/v1alpha2/linodecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ type NetworkSpec struct {
// additionalPorts contains list of ports to be configured with NodeBalancer.
// +optional
AdditionalPorts []LinodeNBPortConfig `json:"additionalPorts,omitempty"`

// UseVlan provisions a cluster that uses VLANs instead of VPCs. IPAM is managed internally.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
UseVlan bool `json:"useVlan,omitempty"`
}

type LinodeNBPortConfig struct {
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha2/linodecluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ func (r *linodeClusterValidator) validateLinodeClusterSpec(ctx context.Context,
}
}

if spec.Network.UseVlan && spec.VPCRef != nil {
errs = append(errs, &field.Error{
Field: "Cannot use VLANs and VPCs together. Unset `network.useVlan` or remove `vpcRef`",
Type: field.ErrorTypeInvalid,
})
}

if len(errs) == 0 {
return nil
}
Expand Down
62 changes: 62 additions & 0 deletions api/v1alpha2/linodecluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,65 @@ func TestValidateDNSLinodeCluster(t *testing.T) {
}),
)
}

func TestValidateVlanAndVPC(t *testing.T) {
t.Parallel()

var (
validCluster = LinodeCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "example",
},
Spec: LinodeClusterSpec{
Region: "us-ord",
Network: NetworkSpec{
UseVlan: true,
},
},
}
inValidCluster = LinodeCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "example",
},
Spec: LinodeClusterSpec{
Region: "us-ord",
Network: NetworkSpec{
UseVlan: true,
},
VPCRef: &corev1.ObjectReference{
Namespace: "example",
Name: "example",
Kind: "LinodeVPC",
},
},
}
validator = &linodeClusterValidator{}
)

NewSuite(t, mock.MockLinodeClient{}).Run(
OneOf(
Path(
Call("valid", func(ctx context.Context, mck Mock) {
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
}),
Result("success", func(ctx context.Context, mck Mock) {
errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, validCluster.Spec)
require.Empty(t, errs)
}),
),
),
OneOf(
Path(Call("vlan and VPC set", func(ctx context.Context, mck Mock) {
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
})),
),
Result("error", func(ctx context.Context, mck Mock) {
errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, inValidCluster.Spec)
for _, err := range errs {
require.Contains(t, err.Error(), "Cannot use VLANs and VPCs together")
}
}),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ spec:
nodeBalancerID:
description: NodeBalancerID is the id of NodeBalancer.
type: integer
useVlan:
description: UseVlan provisions a cluster that uses VLANs instead
of VPCs. IPAM is managed internally.
type: boolean
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
type: object
region:
description: The Linode Region the LinodeCluster lives in.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@ spec:
nodeBalancerID:
description: NodeBalancerID is the id of NodeBalancer.
type: integer
useVlan:
description: UseVlan provisions a cluster that uses VLANs
instead of VPCs. IPAM is managed internally.
type: boolean
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
type: object
region:
description: The Linode Region the LinodeCluster lives in.
Expand Down
2 changes: 2 additions & 0 deletions controller/linodecluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ func (r *LinodeClusterReconciler) reconcileDelete(ctx context.Context, logger lo
return errors.New("waiting for associated LinodeMachine objects to be deleted")
}

util.DeleteClusterIPs(clusterScope.Cluster.Name, clusterScope.Cluster.Namespace)

if err := clusterScope.RemoveCredentialsRefFinalizer(ctx); err != nil {
logger.Error(err, "failed to remove credentials finalizer")
setFailureReason(clusterScope, cerrs.DeleteClusterError, err, r)
Expand Down
17 changes: 17 additions & 0 deletions controller/linodecluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ var _ = Describe("cluster-delete", Ordered, Label("cluster", "cluster-delete"),

cScope := &scope.ClusterScope{
LinodeCluster: &linodeCluster,
Cluster: &clusterv1.Cluster{
ObjectMeta: metadata,
},
}

ctlrSuite.BeforeEach(func(ctx context.Context, mck Mock) {
Expand All @@ -390,8 +393,22 @@ var _ = Describe("cluster-delete", Ordered, Label("cluster", "cluster-delete"),

ctlrSuite.Run(
OneOf(
Path(
Call("cluster with vlan is deleted", func(ctx context.Context, mck Mock) {
cScope.LinodeCluster.Spec.Network.UseVlan = true
cScope.LinodeClient = mck.LinodeClient
cScope.Client = mck.K8sClient
mck.LinodeClient.EXPECT().DeleteNodeBalancer(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
}),
Result("cluster with vlan deleted", func(ctx context.Context, mck Mock) {
reconciler.Client = mck.K8sClient
err := reconciler.reconcileDelete(ctx, logr.Logger{}, cScope)
Expect(err).NotTo(HaveOccurred())
}),
),
Path(
Call("cluster is deleted", func(ctx context.Context, mck Mock) {
cScope.LinodeCluster.Spec.Network.UseVlan = false
cScope.LinodeClient = mck.LinodeClient
cScope.Client = mck.K8sClient
mck.LinodeClient.EXPECT().DeleteNodeBalancer(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
Expand Down
80 changes: 59 additions & 21 deletions controller/linodemachine_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"net/http"
"net/netip"
"slices"
"sort"

Expand Down Expand Up @@ -51,7 +52,10 @@ import (

// Size limit in bytes on the decoded metadata.user_data for cloud-init
// The decoded user_data must not exceed 16384 bytes per the Linode API
const maxBootstrapDataBytes = 16384
const (
maxBootstrapDataBytes = 16384
vlanIPFormat = "%s/11"
)

var (
errNoPublicIPv4Addrs = errors.New("no public ipv4 addresses set")
Expand Down Expand Up @@ -80,24 +84,7 @@ func retryIfTransient(err error) (ctrl.Result, error) {
return ctrl.Result{}, err
}

func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceCreateOptions, error) {
var err error

createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec)
if createConfig == nil {
err = errors.New("failed to convert machine spec to create instance config")

logger.Error(err, "Panic! Struct of LinodeMachineSpec is different than InstanceCreateOptions")

return nil, err
}

createConfig.Booted = util.Pointer(false)

if err := setUserData(ctx, machineScope, createConfig, logger); err != nil {
return nil, err
}

func fillCreateConfig(createConfig *linodego.InstanceCreateOptions, machineScope *scope.MachineScope) {
if machineScope.LinodeMachine.Spec.PrivateIP != nil {
createConfig.PrivateIP = *machineScope.LinodeMachine.Spec.PrivateIP
} else {
Expand All @@ -119,8 +106,28 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg
if createConfig.RootPass == "" {
createConfig.RootPass = uuid.NewString()
}
}

func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceCreateOptions, error) {
var err error

createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec)
if createConfig == nil {
err = errors.New("failed to convert machine spec to create instance config")

logger.Error(err, "Panic! Struct of LinodeMachineSpec is different than InstanceCreateOptions")

return nil, err
}

createConfig.Booted = util.Pointer(false)
if err := setUserData(ctx, machineScope, createConfig, logger); err != nil {
return nil, err
}

fillCreateConfig(createConfig, machineScope)

// if vpc, attach additional interface as eth0 to linode
// if vpc is enabled, attach additional interface as eth0 to linode
if machineScope.LinodeCluster.Spec.VPCRef != nil {
iface, err := getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger)
if err != nil {
Expand All @@ -134,6 +141,15 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg
}
}

// if vlan is enabled, attach additional interface as eth0 to linode
if machineScope.LinodeCluster.Spec.Network.UseVlan {
iface := getVlanInterfaceConfig(machineScope, logger)
if iface != nil {
// add VLAN interface as first interface
createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)
}
}

if machineScope.LinodeMachine.Spec.PlacementGroupRef != nil {
pgID, err := getPlacementGroupID(ctx, machineScope, logger)
if err != nil {
Expand Down Expand Up @@ -191,14 +207,22 @@ func buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, i
Type: clusterv1.MachineExternalIP,
})

// Iterate over interfaces in config and find VPC specific ips
// Iterate over interfaces in config and find VPC or VLAN 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,
})
}

if iface.Purpose == linodego.InterfacePurposeVLAN {
// vlan addresses have a /11 appended to them - we should strip it out.
ips = append(ips, clusterv1.MachineAddress{
Address: netip.MustParsePrefix(iface.IPAMAddress).Addr().String(),
Type: clusterv1.MachineInternalIP,
})
}
}

// if a node has private ip, store it as well
Expand Down Expand Up @@ -345,6 +369,20 @@ func getFirewallID(ctx context.Context, machineScope *scope.MachineScope, logger
return *linodeFirewall.Spec.FirewallID, nil
}

func getVlanInterfaceConfig(machineScope *scope.MachineScope, logger logr.Logger) *linodego.InstanceConfigInterfaceCreateOptions {
logger = logger.WithValues("vlanName", machineScope.Cluster.Name)

// Try to obtain a IP for the machine using its name
ip := util.GetNextVlanIP(machineScope.Cluster.Name, machineScope.Cluster.Namespace)
logger.Info("obtained IP for machine", "name", machineScope.LinodeMachine.Name, "ip", ip)

return &linodego.InstanceConfigInterfaceCreateOptions{
Purpose: linodego.InterfacePurposeVLAN,
Label: machineScope.Cluster.Name,
IPAMAddress: fmt.Sprintf(vlanIPFormat, ip),
}
}

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
Expand Down
2 changes: 1 addition & 1 deletion templates/addons/ccm-linode/ccm-linode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ spec:
repoURL: https://linode.github.io/linode-cloud-controller-manager/
chartName: ccm-linode
namespace: kube-system
version: ${LINODE_CCM_VERSION:=v0.4.14}
version: ${LINODE_CCM_VERSION:=v0.4.16}
options:
waitForJobs: true
wait: true
Expand Down
Loading

0 comments on commit bb94b10

Please sign in to comment.