Skip to content

✨ IPv6 support for self-managed clusters #5603

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions api/v1beta1/awscluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
dst.Status.Bastion.MarketType = restored.Status.Bastion.MarketType
dst.Status.Bastion.HostAffinity = restored.Status.Bastion.HostAffinity
dst.Status.Bastion.HostID = restored.Status.Bastion.HostID
dst.Status.Bastion.IPv6Address = restored.Status.Bastion.IPv6Address
}
dst.Spec.Partition = restored.Spec.Partition

Expand Down
2 changes: 0 additions & 2 deletions api/v1beta1/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ type SubnetSpec struct {

// IPv6CidrBlock is the IPv6 CIDR block to be used when the provider creates a managed VPC.
// A subnet can have an IPv4 and an IPv6 address.
// IPv6 is only supported in managed clusters, this field cannot be set on AWSCluster object.
// +optional
IPv6CidrBlock string `json:"ipv6CidrBlock,omitempty"`

Expand All @@ -261,7 +260,6 @@ type SubnetSpec struct {
IsPublic bool `json:"isPublic"`

// IsIPv6 defines the subnet as an IPv6 subnet. A subnet is IPv6 when it is associated with a VPC that has IPv6 enabled.
// IPv6 is only supported in managed clusters, this field cannot be set on AWSCluster object.
// +optional
IsIPv6 bool `json:"isIpv6,omitempty"`

Expand Down
3 changes: 2 additions & 1 deletion api/v1beta1/zz_generated.conversion.go

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

6 changes: 4 additions & 2 deletions api/v1beta2/awscluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ type Bastion struct {

// AllowedCIDRBlocks is a list of CIDR blocks allowed to access the bastion host.
// They are set as ingress rules for the Bastion host's Security Group (defaults to 0.0.0.0/0).
// If the cluster has IPv6 enabled, defaults to ::/0 and 0.0.0.0/0.
// +optional
AllowedCIDRBlocks []string `json:"allowedCIDRBlocks,omitempty"`
AllowedCIDRBlocks CidrBlocks `json:"allowedCIDRBlocks,omitempty"`

// InstanceType will use the specified instance type for the bastion. If not specified,
// Cluster API Provider AWS will use t3.micro for all regions except us-east-1, where t2.micro
Expand Down Expand Up @@ -323,7 +324,8 @@ type S3Bucket struct {
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Cluster infrastructure is ready for EC2 instances"
// +kubebuilder:printcolumn:name="VPC",type="string",JSONPath=".spec.network.vpc.id",description="AWS VPC the cluster is using"
// +kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.controlPlaneEndpoint",description="API Endpoint",priority=1
// +kubebuilder:printcolumn:name="Bastion IP",type="string",JSONPath=".status.bastion.publicIp",description="Bastion IP address for breakglass access"
// +kubebuilder:printcolumn:name="Bastion IP",type="string",JSONPath=".status.bastion.publicIp",description="Bastion IPv4 address for breakglass access"
// +kubebuilder:printcolumn:name="Bastion IPv6",type="string",JSONPath=".status.bastion.ipv6Address",description="Bastion IPv6 address for breakglass access"
// +k8s:defaulter-gen=true

// AWSCluster is the schema for Amazon EC2 based Kubernetes Cluster API.
Expand Down
38 changes: 31 additions & 7 deletions api/v1beta2/awscluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,35 @@ func (r *AWSCluster) validateSSHKeyName() field.ErrorList {

func (r *AWSCluster) validateNetwork() field.ErrorList {
var allErrs field.ErrorList
if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() {
allErrs = append(allErrs, field.Invalid(field.NewPath("ipv6"), r.Spec.NetworkSpec.VPC.IPv6, "IPv6 cannot be used with unmanaged clusters at this time."))

vpcSpec := r.Spec.NetworkSpec.VPC
vpcField := field.NewPath("spec", "network", "vpc")
if vpcSpec.CidrBlock != "" {
if _, _, err := net.ParseCIDR(vpcSpec.CidrBlock); err != nil {
allErrs = append(allErrs, field.Invalid(vpcField.Child("cidrBlock"), vpcSpec.CidrBlock, "VPC CIDR block is invalid"))
}
}
for _, subnet := range r.Spec.NetworkSpec.Subnets {
if subnet.IsIPv6 || subnet.IPv6CidrBlock != "" {
allErrs = append(allErrs, field.Invalid(field.NewPath("subnets"), r.Spec.NetworkSpec.Subnets, "IPv6 cannot be used with unmanaged clusters at this time."))
if vpcSpec.IPv6 != nil && vpcSpec.IPv6.CidrBlock != "" {
if _, _, err := net.ParseCIDR(vpcSpec.IPv6.CidrBlock); err != nil {
allErrs = append(allErrs, field.Invalid(vpcField.Child("ipv6", "cidrBlock"), vpcSpec.IPv6.CidrBlock, "VPC IPv6 CIDR block is invalid"))
}
}

subnetField := field.NewPath("spec", "network", "subnets")
for i, subnet := range r.Spec.NetworkSpec.Subnets {
if subnet.ZoneType != nil && subnet.IsEdge() {
if subnet.ParentZoneName == nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("subnets"), r.Spec.NetworkSpec.Subnets, "ParentZoneName must be set when ZoneType is 'local-zone'."))
allErrs = append(allErrs, field.Invalid(subnetField.Index(i).Child("parentZoneName"), subnet.ParentZoneName, "ParentZoneName must be set when ZoneType is 'local-zone'."))
}
}
if subnet.CidrBlock != "" {
if _, _, err := net.ParseCIDR(subnet.CidrBlock); err != nil {
allErrs = append(allErrs, field.Invalid(subnetField.Index(i).Child("cidrBlock"), subnet.CidrBlock, "subnet CIDR block is invalid"))
}
}
if subnet.IPv6CidrBlock != "" {
if _, _, err := net.ParseCIDR(subnet.IPv6CidrBlock); err != nil {
allErrs = append(allErrs, field.Invalid(subnetField.Index(i).Child("ipv6CidrBlock"), subnet.IPv6CidrBlock, "subnet IPv6 CIDR block is invalid"))
}
}
}
Expand Down Expand Up @@ -350,10 +369,15 @@ func (r *AWSCluster) validateNetwork() field.ErrorList {

secondaryCidrBlocks := r.Spec.NetworkSpec.VPC.SecondaryCidrBlocks
secondaryCidrBlocksField := field.NewPath("spec", "network", "vpc", "secondaryCidrBlocks")
for _, cidrBlock := range secondaryCidrBlocks {
for i, cidrBlock := range secondaryCidrBlocks {
if r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == cidrBlock.IPv4CidrBlock {
allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSCluster.spec.network.vpc.secondaryCidrBlocks must not contain the primary AWSCluster.spec.network.vpc.cidrBlock %v", r.Spec.NetworkSpec.VPC.CidrBlock)))
}
if cidrBlock.IPv4CidrBlock != "" {
if _, _, err := net.ParseCIDR(cidrBlock.IPv4CidrBlock); err != nil {
allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField.Index(i).Child("ipv4CidrBlock"), cidrBlock.IPv4CidrBlock, "secondary VPC CIDR block is invalid"))
}
}
}

return allErrs
Expand Down
156 changes: 152 additions & 4 deletions api/v1beta2/awscluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,87 @@ func TestAWSClusterValidateCreate(t *testing.T) {
wantErr: false,
},
{
name: "rejects ipv6",
name: "accepts vpc cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
CidrBlock: "10.0.0.0/16",
},
},
},
},
wantErr: false,
},
{
name: "rejects invalid vpc cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
CidrBlock: "10.0.0.0",
},
},
},
},
wantErr: true,
},
{
name: "accepts vpc secondary cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
CidrBlock: "10.0.0.0/16",
SecondaryCidrBlocks: []VpcCidrBlock{
{
IPv4CidrBlock: "10.0.1.0/24",
},
},
},
},
},
},
wantErr: false,
},
{
name: "rejects invalid vpc secondary cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
CidrBlock: "10.0.0.0/16",
SecondaryCidrBlocks: []VpcCidrBlock{
{
IPv4CidrBlock: "10.0.1.0",
},
},
},
},
},
},
wantErr: true,
},
{
name: "rejects vpc secondary cidr duplicate with vpc primary cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
CidrBlock: "10.0.0.0/16",
SecondaryCidrBlocks: []VpcCidrBlock{
{
IPv4CidrBlock: "10.0.0.0/16",
},
},
},
},
},
},
wantErr: true,
},
{
name: "accepts vpc ipv6 cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Expand All @@ -337,10 +417,26 @@ func TestAWSClusterValidateCreate(t *testing.T) {
},
},
},
wantErr: false,
},
{
name: "reject invalid vpc ipv6 cidr",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
VPC: VPCSpec{
IPv6: &IPv6{
CidrBlock: "2001:2345:5678::",
PoolID: "pool-id",
},
},
},
},
},
wantErr: true,
},
{
name: "rejects ipv6 enabled subnet",
name: "accepts ipv6 enabled subnet",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Expand All @@ -356,10 +452,42 @@ func TestAWSClusterValidateCreate(t *testing.T) {
},
},
},
wantErr: false,
},
{
name: "accepts cidr block for subnets",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Subnets: []SubnetSpec{
{
ID: "sub-1",
CidrBlock: "10.0.10.0/24",
},
},
},
},
},
wantErr: false,
},
{
name: "rejects invalid cidr block for subnets",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Subnets: []SubnetSpec{
{
ID: "sub-1",
CidrBlock: "10.0.10.0",
},
},
},
},
},
wantErr: true,
},
{
name: "rejects ipv6 cidr block for subnets",
name: "accepts ipv6 cidr block for subnets",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Expand All @@ -372,6 +500,22 @@ func TestAWSClusterValidateCreate(t *testing.T) {
},
},
},
wantErr: false,
},
{
name: "rejects invalid ipv6 cidr block for subnets",
cluster: &AWSCluster{
Spec: AWSClusterSpec{
NetworkSpec: NetworkSpec{
Subnets: []SubnetSpec{
{
ID: "sub-1",
IPv6CidrBlock: "2022:1234:5678:9101::",
},
},
},
},
},
wantErr: true,
},
{
Expand Down Expand Up @@ -1353,6 +1497,7 @@ func TestAWSClusterValidateAllowedCIDRBlocks(t *testing.T) {
AllowedCIDRBlocks: []string{
"192.168.0.0/16",
"192.168.0.1/32",
"2001:1234:5678:9a40::/56",
},
},
},
Expand All @@ -1379,6 +1524,7 @@ func TestAWSClusterValidateAllowedCIDRBlocks(t *testing.T) {
AllowedCIDRBlocks: []string{
"192.168.0.0/16",
"192.168.0.1/32",
"2001:1234:5678:9a40::/56",
},
DisableIngressRules: true,
},
Expand All @@ -1393,6 +1539,7 @@ func TestAWSClusterValidateAllowedCIDRBlocks(t *testing.T) {
Bastion: Bastion{
AllowedCIDRBlocks: []string{
"100.200.300.400/99",
"2001:1234:5678:9a40::/129",
},
},
},
Expand Down Expand Up @@ -1445,6 +1592,7 @@ func TestAWSClusterDefaultAllowedCIDRBlocks(t *testing.T) {
Bastion: Bastion{
AllowedCIDRBlocks: []string{
"0.0.0.0/0",
"::/0",
},
},
},
Expand All @@ -1455,7 +1603,7 @@ func TestAWSClusterDefaultAllowedCIDRBlocks(t *testing.T) {
beforeCluster: &AWSCluster{
Spec: AWSClusterSpec{
Bastion: Bastion{
AllowedCIDRBlocks: []string{"0.0.0.0/0"},
AllowedCIDRBlocks: []string{"0.0.0.0/0", "::/0"},
DisableIngressRules: true,
Enabled: true,
},
Expand Down
1 change: 1 addition & 0 deletions api/v1beta2/awsmachinetemplate_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func TestAWSMachineTemplateValidateUpdate(t *testing.T) {
InstanceType: "test",
InstanceMetadataOptions: &InstanceMetadataOptions{
HTTPEndpoint: InstanceMetadataEndpointStateEnabled,
HTTPProtocolIPv6: InstanceMetadataEndpointStateDisabled,
HTTPPutResponseHopLimit: 1,
HTTPTokens: HTTPTokensStateOptional,
InstanceMetadataTags: InstanceMetadataEndpointStateDisabled,
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta2/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
func SetDefaults_Bastion(obj *Bastion) { //nolint:golint,stylecheck
// Default to allow open access to the bastion host if no CIDR Blocks have been set
if len(obj.AllowedCIDRBlocks) == 0 && !obj.DisableIngressRules {
obj.AllowedCIDRBlocks = []string{"0.0.0.0/0"}
obj.AllowedCIDRBlocks = []string{"0.0.0.0/0", "::/0"}
}
}

Expand Down
Loading