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

[feat] VLAN support for CAPL clusters #525

Merged
merged 30 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f1962fa
Initial groundwork for VLAN controller
tchinmai7 Sep 27, 2024
1a3ebb7
call auto generated one
tchinmai7 Sep 27, 2024
3d23699
preflight stuff
tchinmai7 Sep 28, 2024
ca234c0
move to networkspec
tchinmai7 Sep 30, 2024
d70ff4a
add kubebuilder annotations
tchinmai7 Sep 30, 2024
8e6245f
Add list permissions
tchinmai7 Sep 30, 2024
4e0bb74
fixups
tchinmai7 Sep 30, 2024
11e3c26
fixups
tchinmai7 Sep 30, 2024
14bb6c1
Add delete perms
tchinmai7 Oct 1, 2024
62058c0
bump ccm to 4.16, add a vlan rke2 flavor
tchinmai7 Oct 2, 2024
c37b625
fixups
tchinmai7 Oct 2, 2024
acfce6d
add some tests
tchinmai7 Oct 2, 2024
a48857f
tests for vlan delete
tchinmai7 Oct 2, 2024
69e345d
fixup
tchinmai7 Oct 2, 2024
e145140
Merge branch 'main' into add-vlan-spec
tchinmai7 Oct 2, 2024
789d49b
lint fix
tchinmai7 Oct 3, 2024
d62b229
fix validator
tchinmai7 Oct 3, 2024
e77147d
lint fix for real
tchinmai7 Oct 3, 2024
f1cd17b
suggestions from review
tchinmai7 Oct 3, 2024
82de6a2
fix template
tchinmai7 Oct 3, 2024
f6ecb55
simplify
tchinmai7 Oct 3, 2024
a366ba7
base flavor on default
tchinmai7 Oct 3, 2024
171a0c9
Merge branch 'main' into add-vlan-spec
tchinmai7 Oct 3, 2024
a8879f4
use a global map instead of a configmap'
tchinmai7 Oct 3, 2024
71b6180
remove code
tchinmai7 Oct 3, 2024
5e6b217
add vlan helpers
tchinmai7 Oct 3, 2024
24a121a
get rid of error
tchinmai7 Oct 3, 2024
c9e8983
Merge branch 'main' into add-vlan-spec
tchinmai7 Oct 4, 2024
392b7e8
add tests and a per cluster lock
tchinmai7 Oct 4, 2024
00c3dc3
Update api/v1alpha2/linodecluster_types.go
tchinmai7 Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"errors"
"fmt"
"net/http"
"net/netip"
"slices"
"sort"

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

// 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 @@
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 @@
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")

Check warning on line 116 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L116

Added line #L116 was not covered by tests

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

Check warning on line 118 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L118

Added line #L118 was not covered by tests

return nil, err

Check warning on line 120 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L120

Added line #L120 was not covered by tests
}

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

Check warning on line 125 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L125

Added line #L125 was not covered by tests
}

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 @@
}
}

// if vlan is enabled, attach additional interface as eth0 to linode
if machineScope.LinodeCluster.Spec.Network.UseVlan {
iface := getVlanInterfaceConfig(machineScope, logger)
if iface != nil {

Check warning on line 147 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L146-L147

Added lines #L146 - L147 were not covered by tests
// add VLAN interface as first interface
createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)

Check warning on line 149 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L149

Added line #L149 was not covered by tests
}
}

if machineScope.LinodeMachine.Spec.PlacementGroupRef != nil {
pgID, err := getPlacementGroupID(ctx, machineScope, logger)
if err != nil {
Expand Down Expand Up @@ -191,14 +207,22 @@
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,
})

Check warning on line 224 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L221-L224

Added lines #L221 - L224 were not covered by tests
}
}

// if a node has private ip, store it as well
Expand Down Expand Up @@ -345,6 +369,20 @@
return *linodeFirewall.Spec.FirewallID, nil
}

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

Check warning on line 373 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L372-L373

Added lines #L372 - L373 were not covered by tests

// 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)

Check warning on line 377 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L376-L377

Added lines #L376 - L377 were not covered by tests

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

Check warning on line 382 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L379-L382

Added lines #L379 - L382 were not covered by tests
}
}

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
Loading