Skip to content

Commit

Permalink
Merge pull request #1710 from etungsten/eks-ipv6
Browse files Browse the repository at this point in the history
kubelet, pluto: support IPv6
  • Loading branch information
etungsten authored Sep 28, 2021
2 parents faebc99 + 2bf5e10 commit dbaa456
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 64 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ Static pods can be particularly useful when running in standalone mode.
For Kubernetes variants in AWS and VMware, the following are set for you automatically, but you can override them if you know what you're doing!
In AWS, [pluto](sources/api/) sets these based on runtime instance information.
In VMware, Bottlerocket uses [netdog](sources/api/) (for `node-ip`) or relies on [default values](sources/models/src/vmware-k8s-1.21/defaults.d/).
* `settings.kubernetes.node-ip`: The IPv4 address of this node.
* `settings.kubernetes.node-ip`: The IP address of this node.
* `settings.kubernetes.pod-infra-container-image`: The URI of the "pause" container.
* `settings.kubernetes.kube-reserved`: Resources reserved for node components.
* Bottlerocket provides default values for the resources by [schnauzer](sources/api/):
Expand All @@ -378,7 +378,7 @@ In VMware, Bottlerocket uses [netdog](sources/api/) (for `node-ip`) or relies on
For Kubernetes variants in AWS, the following settings are set for you automatically by [pluto](sources/api/).
* `settings.kubernetes.max-pods`: The maximum number of pods that can be scheduled on this node (limited by number of available IPv4 addresses)
* `settings.kubernetes.cluster-dns-ip`: Derived from the EKS IPV4 Service CIDR or the CIDR block of the primary network interface.
* `settings.kubernetes.cluster-dns-ip`: Derived from the EKS Service IP CIDR or the CIDR block of the primary network interface.
#### Amazon ECS settings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
From f8ea814e2d459a900bfb5e6f613dbe521b31515b Mon Sep 17 00:00:00 2001
From: Angus Lees <gus@inodes.org>
Date: Thu, 19 Nov 2020 17:34:07 +1100
Subject: [PATCH] AWS: Include IPv6 addresses in NodeAddresses

---
.../k8s.io/legacy-cloud-providers/aws/aws.go | 52 +++++++++++++++++++
.../legacy-cloud-providers/aws/aws_fakes.go | 8 +++
.../legacy-cloud-providers/aws/aws_test.go | 24 ++++++---
3 files changed, 78 insertions(+), 6 deletions(-)

diff --git a/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go b/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go
index c74eef1199f..ee773f063cb 100644
--- a/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go
+++ b/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go
@@ -24,6 +24,7 @@ import (
"fmt"
"io"
"net"
+ "net/http"
"path"
"regexp"
"sort"
@@ -1439,6 +1440,17 @@ func (c *Cloud) HasClusterID() bool {
return len(c.tagging.clusterID()) > 0
}

+// isAWSNotFound returns true if the error was caused by an AWS API 404 response.
+func isAWSNotFound(err error) bool {
+ if err != nil {
+ var aerr awserr.RequestFailure
+ if errors.As(err, &aerr) {
+ return aerr.StatusCode() == http.StatusNotFound
+ }
+ }
+ return false
+}
+
// NodeAddresses is an implementation of Instances.NodeAddresses.
func (c *Cloud) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.NodeAddress, error) {
if c.selfAWSInstance.nodeName == name || len(name) == 0 {
@@ -1493,6 +1505,27 @@ func (c *Cloud) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.No
}
}

+ // IPv6. Ordered after IPv4 addresses, so legacy code can continue to just use the "first" address in a dual-stack cluster.
+ for _, macID := range macIDs {
+ ipPath := path.Join("network/interfaces/macs/", macID, "ipv6s")
+ ips, err := c.metadata.GetMetadata(ipPath)
+ if err != nil {
+ if isAWSNotFound(err) {
+ // No IPv6 configured. Not an error, just a disappointment.
+ continue
+ }
+ return nil, fmt.Errorf("error querying AWS metadata for %q: %q", ipPath, err)
+ }
+
+ for _, ip := range strings.Split(ips, "\n") {
+ if ip == "" {
+ continue
+ }
+ // NB: "Internal" is actually about intra-cluster reachability, and not public vs private.
+ addresses = append(addresses, v1.NodeAddress{Type: v1.NodeInternalIP, Address: ip})
+ }
+ }
+
externalIP, err := c.metadata.GetMetadata("public-ipv4")
if err != nil {
//TODO: It would be nice to be able to determine the reason for the failure,
@@ -1582,6 +1615,25 @@ func extractNodeAddresses(instance *ec2.Instance) ([]v1.NodeAddress, error) {
}
}

+ // IPv6. Ordered after IPv4 addresses, so legacy code can continue to just use the "first" address.
+ for _, networkInterface := range instance.NetworkInterfaces {
+ // skip network interfaces that are not currently in use
+ if aws.StringValue(networkInterface.Status) != ec2.NetworkInterfaceStatusInUse {
+ continue
+ }
+
+ for _, addr6 := range networkInterface.Ipv6Addresses {
+ if ipAddress := aws.StringValue(addr6.Ipv6Address); ipAddress != "" {
+ ip := net.ParseIP(ipAddress)
+ if ip == nil {
+ return nil, fmt.Errorf("EC2 instance had invalid IPv6 address: %s (%q)", aws.StringValue(instance.InstanceId), ipAddress)
+ }
+ // NB: "Internal" is actually about intra-cluster reachability, and not public vs private.
+ addresses = append(addresses, v1.NodeAddress{Type: v1.NodeInternalIP, Address: ip.String()})
+ }
+ }
+ }
+
// TODO: Other IP addresses (multiple ips)?
publicIPAddress := aws.StringValue(instance.PublicIpAddress)
if publicIPAddress != "" {
diff --git a/staging/src/k8s.io/legacy-cloud-providers/aws/aws_fakes.go b/staging/src/k8s.io/legacy-cloud-providers/aws/aws_fakes.go
index 0113c55554f..500a81a696a 100644
--- a/staging/src/k8s.io/legacy-cloud-providers/aws/aws_fakes.go
+++ b/staging/src/k8s.io/legacy-cloud-providers/aws/aws_fakes.go
@@ -39,6 +39,7 @@ type FakeAWSServices struct {
selfInstance *ec2.Instance
networkInterfacesMacs []string
networkInterfacesPrivateIPs [][]string
+ networkInterfacesIPv6s [][]string
networkInterfacesVpcIDs []string

ec2 FakeEC2
@@ -374,6 +375,13 @@ func (m *FakeMetadata) GetMetadata(key string) (string, error) {
}
}
}
+ if len(keySplit) == 5 && keySplit[4] == "ipv6s" {
+ for i, macElem := range m.aws.networkInterfacesMacs {
+ if macParam == macElem {
+ return strings.Join(m.aws.networkInterfacesIPv6s[i], "/\n"), nil
+ }
+ }
+ }

return "", nil
}
diff --git a/staging/src/k8s.io/legacy-cloud-providers/aws/aws_test.go b/staging/src/k8s.io/legacy-cloud-providers/aws/aws_test.go
index 56fd43dd12c..40c570d7bf6 100644
--- a/staging/src/k8s.io/legacy-cloud-providers/aws/aws_test.go
+++ b/staging/src/k8s.io/legacy-cloud-providers/aws/aws_test.go
@@ -583,7 +583,7 @@ func testHasNodeAddress(t *testing.T, addrs []v1.NodeAddress, addressType v1.Nod
t.Errorf("Did not find expected address: %s:%s in %v", addressType, address, addrs)
}

-func makeInstance(num int, privateIP, publicIP, privateDNSName, publicDNSName string, setNetInterface bool) ec2.Instance {
+func makeInstance(num int, privateIP, publicIP, ipv6IP, privateDNSName, publicDNSName string, setNetInterface bool) ec2.Instance {
var tag ec2.Tag
tag.Key = aws.String(TagNameKubernetesClusterLegacy)
tag.Value = aws.String(TestClusterID)
@@ -613,6 +613,14 @@ func makeInstance(num int, privateIP, publicIP, privateDNSName, publicDNSName st
},
},
}
+
+ if ipv6IP != "" {
+ instance.NetworkInterfaces[0].Ipv6Addresses = []*ec2.InstanceIpv6Address{
+ {
+ Ipv6Address: aws.String(ipv6IP),
+ },
+ }
+ }
}
return instance
}
@@ -620,9 +628,9 @@ func makeInstance(num int, privateIP, publicIP, privateDNSName, publicDNSName st
func TestNodeAddresses(t *testing.T) {
// Note instance0 and instance1 have the same name
// (we test that this produces an error)
- instance0 := makeInstance(0, "192.168.0.1", "1.2.3.4", "instance-same.ec2.internal", "instance-same.ec2.external", true)
- instance1 := makeInstance(1, "192.168.0.2", "", "instance-same.ec2.internal", "", false)
- instance2 := makeInstance(2, "192.168.0.1", "1.2.3.4", "instance-other.ec2.internal", "", false)
+ instance0 := makeInstance(0, "192.168.0.1", "1.2.3.4", "2001:db8::1", "instance-same.ec2.internal", "instance-same.ec2.external", true)
+ instance1 := makeInstance(1, "192.168.0.2", "", "", "instance-same.ec2.internal", "", false)
+ instance2 := makeInstance(2, "192.168.0.1", "1.2.3.4", "", "instance-other.ec2.internal", "", false)
instances := []*ec2.Instance{&instance0, &instance1, &instance2}

aws1, _ := mockInstancesResp(&instance0, []*ec2.Instance{&instance0})
@@ -644,23 +652,25 @@ func TestNodeAddresses(t *testing.T) {
if err3 != nil {
t.Errorf("Should not error when instance found")
}
- if len(addrs3) != 5 {
+ if len(addrs3) != 6 {
t.Errorf("Should return exactly 5 NodeAddresses")
}
testHasNodeAddress(t, addrs3, v1.NodeInternalIP, "192.168.0.1")
testHasNodeAddress(t, addrs3, v1.NodeExternalIP, "1.2.3.4")
+ testHasNodeAddress(t, addrs3, v1.NodeInternalIP, "2001:db8::1")
testHasNodeAddress(t, addrs3, v1.NodeExternalDNS, "instance-same.ec2.external")
testHasNodeAddress(t, addrs3, v1.NodeInternalDNS, "instance-same.ec2.internal")
testHasNodeAddress(t, addrs3, v1.NodeHostName, "instance-same.ec2.internal")
}

func TestNodeAddressesWithMetadata(t *testing.T) {
- instance := makeInstance(0, "", "2.3.4.5", "instance.ec2.internal", "", false)
+ instance := makeInstance(0, "", "2.3.4.5", "", "instance.ec2.internal", "", false)
instances := []*ec2.Instance{&instance}
awsCloud, awsServices := mockInstancesResp(&instance, instances)

awsServices.networkInterfacesMacs = []string{"0a:77:89:f3:9c:f6", "0a:26:64:c4:6a:48"}
awsServices.networkInterfacesPrivateIPs = [][]string{{"192.168.0.1"}, {"192.168.0.2"}}
+ awsServices.networkInterfacesIPv6s = [][]string{{"2001:db8:1::1"}, {"2001:db8:1::2"}}
addrs, err := awsCloud.NodeAddresses(context.TODO(), "")
if err != nil {
t.Errorf("unexpected error: %v", err)
@@ -668,6 +678,8 @@ func TestNodeAddressesWithMetadata(t *testing.T) {
testHasNodeAddress(t, addrs, v1.NodeInternalIP, "192.168.0.1")
testHasNodeAddress(t, addrs, v1.NodeInternalIP, "192.168.0.2")
testHasNodeAddress(t, addrs, v1.NodeExternalIP, "2.3.4.5")
+ testHasNodeAddress(t, addrs, v1.NodeInternalIP, "2001:db8:1::1")
+ testHasNodeAddress(t, addrs, v1.NodeInternalIP, "2001:db8:1::2")
var index1, index2 int
for i, addr := range addrs {
if addr.Type == v1.NodeInternalIP && addr.Address == "192.168.0.1" {
--
2.17.1

2 changes: 2 additions & 0 deletions packages/kubernetes-1.21/kubernetes-1.21.spec
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Source8: kubernetes-tmpfiles.conf
Source9: kubelet-sysctl.conf
Source1000: clarify.toml

Patch0001: 0001-AWS-Include-IPv6-addresses-in-NodeAddresses.patch

BuildRequires: git
BuildRequires: rsync
BuildRequires: %{_cross_os}glibc-devel
Expand Down
6 changes: 6 additions & 0 deletions packages/release/release-sysctl.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ net.ipv4.ip_default_ttl = 255
# Enable IPv4 forwarding for container networking.
net.ipv4.conf.all.forwarding = 1

# Enable IPv6 forwarding for container networking.
net.ipv6.conf.all.forwarding = 1

# Accept router advertisement (RA) packets even if IPv6 forwarding is enabled on eth0
net.ipv6.conf.eth0.accept_ra = 2

# This is generally considered a safe ephemeral port range
net.ipv4.ip_local_port_range = 32768 60999

Expand Down
2 changes: 1 addition & 1 deletion sources/api/pluto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ It uses IMDS to get information such as:

It uses EKS to get information such as:

- Service IPV4 CIDR
- Service IP CIDR

It uses the Bottlerocket API to get information such as:

Expand Down
13 changes: 7 additions & 6 deletions sources/api/pluto/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
pub(super) use inner::{get_aws_k8s_info, Error};
use std::net::IpAddr;

/// The result type for the [`api`] module.
pub(super) type Result<T> = std::result::Result<T, Error>;

#[derive(Default)]
pub(crate) struct AwsK8sInfo {
pub(crate) region: String,
pub(crate) cluster_name: String,
pub(crate) cluster_dns_ip: Option<IpAddr>,
}

/// This code is the 'actual' implementation compiled when the `sources` workspace is being compiled
Expand Down Expand Up @@ -47,23 +48,23 @@ mod inner {
/// Gets the info that we need to know about the EKS cluster from the Bottlerocket API.
pub(crate) async fn get_aws_k8s_info() -> Result<AwsK8sInfo> {
let settings = get_settings().await?;
let kubernetes = settings.kubernetes.context(Missing {
setting: "kubernetes",
})?;
Ok(AwsK8sInfo {
region: settings
.aws
.context(Missing { setting: "aws" })?
.region
.context(Missing { setting: "region" })?
.into(),
cluster_name: settings
.kubernetes
.context(Missing {
setting: "kubernetes",
})?
cluster_name: kubernetes
.cluster_name
.context(Missing {
setting: "cluster-name",
})?
.into(),
cluster_dns_ip: kubernetes.cluster_dns_ip,
})
}
}
Expand Down
18 changes: 10 additions & 8 deletions sources/api/pluto/src/eks.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use rusoto_core::region::ParseRegionError;
use rusoto_core::{Region, RusotoError};
use rusoto_eks::{DescribeClusterError, Eks, EksClient};
use rusoto_eks::{DescribeClusterError, Eks, EksClient, KubernetesNetworkConfigResponse};
use snafu::{OptionExt, ResultExt, Snafu};
use std::str::FromStr;

pub(crate) type ClusterNetworkConfig = KubernetesNetworkConfigResponse;

#[derive(Debug, Snafu)]
pub(super) enum Error {
#[snafu(display("Error describing cluster: {}", source))]
Expand All @@ -23,14 +25,18 @@ pub(super) enum Error {

type Result<T> = std::result::Result<T, Error>;

/// Returns the cluster's [serviceIPv4CIDR] DNS IP by calling the EKS API.
/// (https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigRequest.html)
pub(super) async fn get_cluster_cidr(region: &str, cluster: &str) -> Result<String> {
/// Returns the cluster's [kubernetesNetworkConfig] by calling the EKS API.
/// (https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigResponse.html)
pub(super) async fn get_cluster_network_config(
region: &str,
cluster: &str,
) -> Result<ClusterNetworkConfig> {
let parsed_region = Region::from_str(region).context(RegionParse { region })?;
let client = EksClient::new(parsed_region);
let describe_cluster = rusoto_eks::DescribeClusterRequest {
name: cluster.to_owned(),
};

client
.describe_cluster(describe_cluster)
.await
Expand All @@ -40,9 +46,5 @@ pub(super) async fn get_cluster_cidr(region: &str, cluster: &str) -> Result<Stri
.kubernetes_network_config
.context(Missing {
field: "kubernetes_network_config",
})?
.service_ipv_4_cidr
.context(Missing {
field: "service_ipv_4_cidr",
})
}
Loading

0 comments on commit dbaa456

Please sign in to comment.