Skip to content

[WIP][feat] : allocate ipv6 /64 cidrs to linodes #773

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions api/v1alpha2/linodevpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ type LinodeVPCSpec struct {
// supplied then the credentials of the controller will be used.
// +optional
CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
// +kubebuilder:validation:MinLength=2
// +kubebuilder:validation:MaxLength=4
// +kubebuilder:default="/52"
IPv6Range string `json:"ipv6Range,omitempty"`
}

// VPCSubnetCreateOptions defines subnet options
Expand All @@ -55,6 +61,11 @@ type VPCSubnetCreateOptions struct {
// SubnetID is subnet id for the subnet
// +optional
SubnetID int `json:"subnetID,omitempty"`
// +optional
// +kubebuilder:validation:MinLength=2
// +kubebuilder:validation:MaxLength=4
// +kubebuilder:default="/56"
IPv6Range string `json:"ipv6Range,omitempty"`
}

// LinodeVPCStatus defines the observed state of LinodeVPC
Expand Down
13 changes: 13 additions & 0 deletions config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ spec:
x-kubernetes-map-type: atomic
description:
type: string
ipv6Range:
default: /52
maxLength: 4
minLength: 2
type: string
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
region:
type: string
x-kubernetes-validations:
Expand All @@ -78,6 +86,11 @@ spec:
properties:
ipv4:
type: string
ipv6Range:
default: /56
maxLength: 4
minLength: 2
type: string
label:
maxLength: 63
minLength: 3
Expand Down
2 changes: 2 additions & 0 deletions docs/src/reference/out.md
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@ _Appears in:_
| `region` _string_ | | | |
| `subnets` _[VPCSubnetCreateOptions](#vpcsubnetcreateoptions) array_ | | | |
| `credentialsRef` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#secretreference-v1-core)_ | CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this VPC. If not<br />supplied then the credentials of the controller will be used. | | |
| `ipv6Range` _string_ | | /52 | MaxLength: 4 <br />MinLength: 2 <br /> |


#### LinodeVPCStatus
Expand Down Expand Up @@ -1237,5 +1238,6 @@ _Appears in:_
| `label` _string_ | | | MaxLength: 63 <br />MinLength: 3 <br /> |
| `ipv4` _string_ | | | |
| `subnetID` _integer_ | SubnetID is subnet id for the subnet | | |
| `ipv6Range` _string_ | | /56 | MaxLength: 4 <br />MinLength: 2 <br /> |


2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/linode/linodego => github.com/rahulait/linodego v1.50.1-0.20250708032219-cf7181d2963f
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/linode/linodego v1.52.2 h1:N9ozU27To1LMSrDd8WvJZ5STSz1eGYdyLnxhAR/dIZg=
github.com/linode/linodego v1.52.2/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
Expand Down Expand Up @@ -223,6 +221,8 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rahulait/linodego v1.50.1-0.20250708032219-cf7181d2963f h1:16z+vd4lkTN/jDfZB5KpG0j2MM+Q6m/ZAb0EugSFuPU=
github.com/rahulait/linodego v1.50.1-0.20250708032219-cf7181d2963f/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
Expand Down
15 changes: 15 additions & 0 deletions internal/controller/linodemachine_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
const (
maxBootstrapDataBytesCloudInit = 16384
vlanIPFormat = "%s/11"
ipv6Range = "/64" // Default IPv6 range for VPC interfaces
)

var (
Expand Down Expand Up @@ -494,6 +495,13 @@ func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope
IPv4: &linodego.VPCIPv4{
NAT1To1: ptr.To("any"),
},
IPv6: &linodego.InstanceConfigInterfaceCreateOptionsIPv6{
Ranges: []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range{
{
Range: ptr.To(ipv6Range),
},
},
},
}, nil
}

Expand Down Expand Up @@ -549,6 +557,13 @@ func getVPCInterfaceConfigFromDirectID(ctx context.Context, machineScope *scope.
IPv4: &linodego.VPCIPv4{
NAT1To1: ptr.To("any"),
},
IPv6: &linodego.InstanceConfigInterfaceCreateOptionsIPv6{
Ranges: []linodego.InstanceConfigInterfaceCreateOptionsIPv6Range{
{
Range: ptr.To(ipv6Range),
},
},
},
}, nil
}

Expand Down
13 changes: 13 additions & 0 deletions internal/controller/linodevpc_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,28 @@ func updateVPCSpecSubnets(vpcScope *scope.VPCScope, vpc *linodego.VPC) {

func linodeVPCSpecToVPCCreateConfig(vpcSpec infrav1alpha2.LinodeVPCSpec) *linodego.VPCCreateOptions {
subnets := make([]linodego.VPCSubnetCreateOptions, len(vpcSpec.Subnets))
vpcIPv6 := []linodego.VPCCreateOptionsIPv6{
{
Range: &vpcSpec.IPv6Range,
},
}

for idx, subnet := range vpcSpec.Subnets {
subnets[idx] = linodego.VPCSubnetCreateOptions{
Label: subnet.Label,
IPv4: subnet.IPv4,
IPv6: []linodego.VPCSubnetCreateOptionsIPv6{
{
Range: &subnet.IPv6Range,
},
},
}
}

return &linodego.VPCCreateOptions{
Description: vpcSpec.Description,
Region: vpcSpec.Region,
Subnets: subnets,
IPv6: vpcIPv6,
}
}
51 changes: 45 additions & 6 deletions internal/webhook/v1alpha2/linodevpc_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/netip"
"regexp"
"slices"
"strconv"
"strings"

"go4.org/netipx"
Expand Down Expand Up @@ -146,7 +147,11 @@ func (r *linodeVPCValidator) ValidateDelete(ctx context.Context, obj runtime.Obj

func (r *linodeVPCValidator) validateLinodeVPCSpec(ctx context.Context, linodeclient clients.LinodeClient, spec infrav1alpha2.LinodeVPCSpec, skipAPIValidation bool) field.ErrorList {
// TODO: instrument with tracing, might need refactor to preserve readibility
var errs field.ErrorList
var (
errs field.ErrorList
ipv6Range = spec.IPv6Range
ipv6RangePath = field.NewPath("spec").Child("ipv6Range")
)

if !skipAPIValidation {
if err := validateRegion(ctx, linodeclient, spec.Region, field.NewPath("spec").Child("region"), LinodeVPCCapability); err != nil {
Expand All @@ -157,6 +162,12 @@ func (r *linodeVPCValidator) validateLinodeVPCSpec(ctx context.Context, linodecl
errs = slices.Concat(errs, err)
}

// Validate VPC IPv6 Range
rangeErr := validateIPv6Range(ipv6Range, ipv6RangePath)
if rangeErr != nil {
errs = append(errs, rangeErr)
}

if len(errs) == 0 {
return nil
}
Expand All @@ -171,12 +182,14 @@ func (r *linodeVPCValidator) validateLinodeVPCSubnets(spec infrav1alpha2.LinodeV
labels = []string{}
)

for i, subnet := range spec.Subnets {
for idx, subnet := range spec.Subnets {
var (
label = subnet.Label
labelPath = field.NewPath("spec").Child("Subnets").Index(i).Child("Label")
ip = subnet.IPv4
ipPath = field.NewPath("spec").Child("Subnets").Index(i).Child("IPv4")
label = subnet.Label
labelPath = field.NewPath("spec").Child("Subnets").Index(idx).Child("Label")
ip = subnet.IPv4
ipPath = field.NewPath("spec").Child("Subnets").Index(idx).Child("IPv4")
ipv6Range = subnet.IPv6Range
ipv6RangePath = field.NewPath("spec").Child("Subnets").Index(idx).Child("IPv6Range")
)

// Validate Subnet Label
Expand All @@ -202,6 +215,12 @@ func (r *linodeVPCValidator) validateLinodeVPCSubnets(spec infrav1alpha2.LinodeV
if cidrs, err = builder.IPSet(); err != nil {
return append(field.ErrorList{}, field.InternalError(ipPath, fmt.Errorf("build ip set: %w", err)))
}

// Validate Subnet IPv6 Range
rangeErr := validateIPv6Range(ipv6Range, ipv6RangePath)
if rangeErr != nil {
errs = append(errs, rangeErr)
}
}

if len(errs) == 0 {
Expand Down Expand Up @@ -282,3 +301,23 @@ func validateSubnetIPv4CIDR(cidr string, path *field.Path) (*netipx.IPSet, *fiel
}
return set, nil
}

func validateIPv6Range(ipv6Range string, path *field.Path) *field.Error {
var errs = []error{
errors.New("IPv6 range must start with /. Example: /52"),
errors.New("IPv6 range doesn't contain a valid number after /"),
errors.New("IPv6 range must be between /0 and /128"),
}
if !strings.HasPrefix(ipv6Range, "/") {
return field.Invalid(path, ipv6Range, errs[0].Error())
}
numStr := strings.TrimPrefix(ipv6Range, "/")
num, err := strconv.Atoi(numStr)
if err != nil {
return field.Invalid(path, ipv6Range, errs[1].Error())
}
if num < 0 || num > 128 {
return field.Invalid(path, ipv6Range, errs[2].Error())
}
return nil
}
Loading