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

Initial groundwork for VLAN controller #525

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -86,3 +86,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 @@ -43,6 +43,11 @@ type LinodeClusterSpec struct {
// +optional
Network NetworkSpec `json:"network"`

// 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"`
AshleyDumaine marked this conversation as resolved.
Show resolved Hide resolved

// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
VPCRef *corev1.ObjectReference `json:"vpcRef,omitempty"`
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 @@ -106,6 +106,13 @@
}
}

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

Check warning on line 113 in api/v1alpha2/linodecluster_webhook.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha2/linodecluster_webhook.go#L110-L113

Added lines #L110 - L113 were not covered by tests
}

if len(errs) == 0 {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ spec:
region:
description: The Linode Region the LinodeCluster lives in.
type: string
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
vpcRef:
description: |-
ObjectReference contains enough information to let you inspect or modify the referred object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ spec:
region:
description: The Linode Region the LinodeCluster lives in.
type: string
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
vpcRef:
description: |-
ObjectReference contains enough information to let you inspect or modify the referred object.
Expand Down
135 changes: 115 additions & 20 deletions controller/linodemachine_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@
"errors"
"fmt"
"net/http"
"net/netip"
"slices"
"sort"

"github.com/go-logr/logr"
"github.com/google/uuid"
"github.com/linode/linodego"
"golang.org/x/exp/maps"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
cerrs "sigs.k8s.io/cluster-api/errors"
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
}

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

// if vlan is enabled, attach additional interface as eth0 to linode
if machineScope.LinodeCluster.Spec.UseVlan {
iface, err := getVlanInterfaceConfig(ctx, machineScope, logger)
if err != nil {
logger.Error(err, "Failed to get VLAN interface config")
return nil, err

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#L146-L149

Added lines #L146 - L149 were not covered by tests
}
if iface != nil {

Check warning on line 151 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L151

Added line #L151 was not covered by tests
// add VLAN interface as first interface
createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface)

Check warning on line 153 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L153

Added line #L153 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 +211,21 @@
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 {
ips = append(ips, clusterv1.MachineAddress{
Address: iface.IPAMAddress,
Type: clusterv1.MachineInternalIP,
})

Check warning on line 227 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L224-L227

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

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

func getNextIP(ips []string, prefixStr string) string {
prefix := netip.MustParsePrefix(prefixStr)
currentIp := prefix.Addr().Next()

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#L375-L377

Added lines #L375 - L377 were not covered by tests

ipString := currentIp.String()
for {
if slices.Contains(ips, ipString) {
break

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
}
currentIp = currentIp.Next()
ipString = currentIp.String()

Check warning on line 385 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L384-L385

Added lines #L384 - L385 were not covered by tests
}
return ipString

Check warning on line 387 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L387

Added line #L387 was not covered by tests
}

func reserveNextIP(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (string, error) {
namespace := machineScope.Cluster.Namespace
clusterName := machineScope.Cluster.Name

Check warning on line 392 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L390-L392

Added lines #L390 - L392 were not covered by tests

var ipsMap corev1.ConfigMap
AshleyDumaine marked this conversation as resolved.
Show resolved Hide resolved
err := machineScope.Client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: fmt.Sprintf("%s-ips", clusterName)}, &ipsMap)
if err != nil && !apierrors.IsNotFound(err) {
return "", fmt.Errorf("retreiving ips configmap %s/%s: %w", namespace, fmt.Sprintf("%s-ips", clusterName), err)

Check warning on line 397 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L394-L397

Added lines #L394 - L397 were not covered by tests
}

if ip, ok := ipsMap.Data[machineScope.LinodeMachine.Name]; ok {
return ip, nil

Check warning on line 401 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L400-L401

Added lines #L400 - L401 were not covered by tests
}

var nextIP string
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
if err = machineScope.Client.Get(ctx, client.ObjectKeyFromObject(&ipsMap), &ipsMap); err != nil {
return fmt.Errorf("retreiving ips configmap %s/%s: %w", namespace, fmt.Sprintf("%s-ips", clusterName), err)

Check warning on line 407 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L404-L407

Added lines #L404 - L407 were not covered by tests
}

nextIP = getNextIP(maps.Values(ipsMap.Data), "10.0.0.0/11")
ipsMap.Data[machineScope.LinodeMachine.Name] = nextIP
if err := machineScope.Client.Update(ctx, &ipsMap); err != nil {
return fmt.Errorf("updating ips configMap: %w", err)

Check warning on line 413 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L410-L413

Added lines #L410 - L413 were not covered by tests
}
return nil

Check warning on line 415 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L415

Added line #L415 was not covered by tests
})
if err != nil {
return "", err

Check warning on line 418 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L417-L418

Added lines #L417 - L418 were not covered by tests
}

logger.Info("onbained IP for machine", "name", machineScope.LinodeMachine.Name, "ip", nextIP)

Check warning on line 421 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L421

Added line #L421 was not covered by tests
// if an IP is available
return nextIP, nil

Check warning on line 423 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L423

Added line #L423 was not covered by tests
}

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

Check warning on line 427 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L426-L427

Added lines #L426 - L427 were not covered by tests

// Try to obtain a IP for the machine using its name

ip, err := reserveNextIP(ctx, machineScope, logger)
if err != nil {
return nil, err

Check warning on line 433 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L431-L433

Added lines #L431 - L433 were not covered by tests
}

return &linodego.InstanceConfigInterfaceCreateOptions{
Purpose: linodego.InterfacePurposeVLAN,
Label: machineScope.Cluster.Name,
IPAMAddress: ip,
}, nil

Check warning on line 440 in controller/linodemachine_controller_helpers.go

View check run for this annotation

Codecov / codecov/patch

controller/linodemachine_controller_helpers.go#L436-L440

Added lines #L436 - L440 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
Loading