diff --git a/README.md b/README.md index 86471c96b..8dcb68197 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,11 @@ You might also need to provide your DigitalOcean access token in the cloud controller to start, but in that case, you will not be able to validate integration with DigitalOcean API. +The `DO_IP_ADDR_FAMILIES` is used to configure the required IP families and the +order in which the addresses should be populated in nodes status. The accepted values +are one of the `"ipv4", "ipv6"` or a comma-separated list of multiple IP address +families. IPv4 is the default, if not set or empty. + Please note that if you use a Kubernetes cluster created on DigitalOcean, there will be a cloud controller manager running in the cluster already, so your local one will compete for API access with it. diff --git a/cloud-controller-manager/do/cloud.go b/cloud-controller-manager/do/cloud.go index e7618e45c..32cfedfe8 100644 --- a/cloud-controller-manager/do/cloud.go +++ b/cloud-controller-manager/do/cloud.go @@ -57,6 +57,7 @@ const ( publicAccessFirewallTagsEnv string = "PUBLIC_ACCESS_FIREWALL_TAGS" regionEnv string = "REGION" doAPIRateLimitQPSEnv string = "DO_API_RATE_LIMIT_QPS" + doIPAddrFamiliesEnv string = "DO_IP_ADDR_FAMILIES" ) var version string @@ -158,6 +159,11 @@ func newCloud() (cloudprovider.Interface, error) { addr = fmt.Sprintf("%s:%s", addrHost, addrPort) } + ipf, set := os.LookupEnv(doIPAddrFamiliesEnv) + if set && !validateAndSetIPFamilies(ipf) { + return nil, fmt.Errorf("invalid value set for environment variable %q", doIPAddrFamiliesEnv) + } + return &cloud{ client: doClient, instances: newInstances(resources, region), @@ -280,3 +286,15 @@ func (c *cloud) ScrubDNS(nameservers, searches []string) (nsOut, srchOut []strin func (c *cloud) HasClusterID() bool { return false } + +func validateAndSetIPFamilies(ipf string) bool { + for _, v := range strings.Split(ipf, ",") { + ipf := strings.TrimSpace(v) + if ipf == ipv4Family || ipf == ipv6Family { + ipFamilies = append(ipFamilies, IPFamily(ipf)) + } else { + return false + } + } + return true +} diff --git a/cloud-controller-manager/do/common.go b/cloud-controller-manager/do/common.go index 1e0538c60..e4638e133 100644 --- a/cloud-controller-manager/do/common.go +++ b/cloud-controller-manager/do/common.go @@ -25,6 +25,15 @@ import ( v1 "k8s.io/api/core/v1" ) +type IPFamily string + +var ipFamilies []IPFamily + +const ( + ipv4Family = "ipv4" + ipv6Family = "ipv6" +) + // apiResultsPerPage is the maximum page size that DigitalOcean's api supports. const apiResultsPerPage = 200 @@ -129,17 +138,50 @@ func nodeAddresses(droplet *godo.Droplet) ([]v1.NodeAddress, error) { var addresses []v1.NodeAddress addresses = append(addresses, v1.NodeAddress{Type: v1.NodeHostName, Address: droplet.Name}) - privateIP, err := droplet.PrivateIPv4() - if err != nil || privateIP == "" { - return nil, fmt.Errorf("could not get private ip: %v", err) + // default case when DO_IP_ADDR_FAMILIES is not set + if ipFamilies == nil { + addr, err := discoverAddress(droplet, ipv4Family) + if err != nil { + return nil, fmt.Errorf("could not get addresses for %s : %v", ipv4Family, err) + } + addresses = append(addresses, addr...) + } else { + for _, i := range ipFamilies { + addr, err := discoverAddress(droplet, i) + if err != nil { + return nil, fmt.Errorf("could not get addresses for %s : %v", i, err) + } + addresses = append(addresses, addr...) + } } - addresses = append(addresses, v1.NodeAddress{Type: v1.NodeInternalIP, Address: privateIP}) - publicIP, err := droplet.PublicIPv4() - if err != nil || publicIP == "" { - return nil, fmt.Errorf("could not get public ip: %v", err) - } - addresses = append(addresses, v1.NodeAddress{Type: v1.NodeExternalIP, Address: publicIP}) + return addresses, nil +} +func discoverAddress(droplet *godo.Droplet, family IPFamily) ([]v1.NodeAddress, error) { + var addresses []v1.NodeAddress + + switch family { + case ipv4Family: + privateIP, err := droplet.PrivateIPv4() + if err != nil || privateIP == "" { + return nil, fmt.Errorf("could not get private ip: %v", err) + } + addresses = append(addresses, v1.NodeAddress{Type: v1.NodeInternalIP, Address: privateIP}) + + publicIP, err := droplet.PublicIPv4() + if err != nil || publicIP == "" { + return nil, fmt.Errorf("could not get public ip: %v", err) + } + addresses = append(addresses, v1.NodeAddress{Type: v1.NodeExternalIP, Address: publicIP}) + return addresses, nil + case ipv6Family: + publicIPv6, err := droplet.PublicIPv6() + if err != nil || publicIPv6 == "" { + return nil, fmt.Errorf("could not get public ipv6: %v", err) + } + addresses = append(addresses, v1.NodeAddress{Type: v1.NodeExternalIP, Address: publicIPv6}) + return addresses, nil + } return addresses, nil } diff --git a/cloud-controller-manager/do/droplets_test.go b/cloud-controller-manager/do/droplets_test.go index d73a9b4da..5c213fa72 100644 --- a/cloud-controller-manager/do/droplets_test.go +++ b/cloud-controller-manager/do/droplets_test.go @@ -118,6 +118,12 @@ func newFakeDroplet() *godo.Droplet { Type: "public", }, }, + V6: []godo.NetworkV6{ + { + IPAddress: "2a01::10", + Type: "public", + }, + }, }, Region: &godo.Region{ Name: "test-region", @@ -143,6 +149,12 @@ func newFakeShutdownDroplet() *godo.Droplet { Type: "public", }, }, + V6: []godo.NetworkV6{ + { + IPAddress: "2a01::10", + Type: "public", + }, + }, }, Region: &godo.Region{ Name: "test-region", @@ -186,8 +198,12 @@ func TestNodeAddresses(t *testing.T) { Type: v1.NodeExternalIP, Address: "99.99.99.99", }, + { + Type: v1.NodeExternalIP, + Address: "2a01::10", + }, } - + ipFamilies = []IPFamily{ipv4Family, ipv6Family} addresses, err := instances.NodeAddresses(context.TODO(), "test-droplet") if !reflect.DeepEqual(addresses, expectedAddresses) { @@ -223,7 +239,7 @@ func TestNodeAddressesByProviderID(t *testing.T) { Address: "99.99.99.99", }, } - + ipFamilies = []IPFamily{ipv4Family} addresses, err := instances.NodeAddressesByProviderID(context.TODO(), "digitalocean://123") if !reflect.DeepEqual(addresses, expectedAddresses) { diff --git a/docs/controllers/node/examples/README.md b/docs/controllers/node/examples/README.md index aaf5cc30f..c1034389b 100644 --- a/docs/controllers/node/examples/README.md +++ b/docs/controllers/node/examples/README.md @@ -68,6 +68,8 @@ status: type: InternalIP - address: 138.197.174.81 type: ExternalIP + - address: 2a03:b0c0:3:d0::e68:a001 + type: ExternalIP allocatable: cpu: "4" memory: 6012700Ki @@ -80,7 +82,7 @@ status: DigitalOcean cloud controller manager has made the cluster aware of the size of the node, in this case c-4 (4 core high CPU droplet). It has also assigned the node a failure domain which the scheduler can use for region failovers. Note also that the correct addresses were assigned to the node. The `InternalIP` now represents -the private IP of the droplet, and the `ExternalIP` is it's public IP. +the private IP of the droplet, and the `ExternalIP` is it's public IP. The order and IP families depends on the env variable `DO_IP_ADDR_FAMILIES`. ## Node clean up diff --git a/docs/example-manifests/cloud-controller-manager.yml b/docs/example-manifests/cloud-controller-manager.yml index cb82c126e..d76b456de 100644 --- a/docs/example-manifests/cloud-controller-manager.yml +++ b/docs/example-manifests/cloud-controller-manager.yml @@ -66,6 +66,8 @@ spec: secretKeyRef: name: digitalocean key: access-token + - name: DO_IP_ADDR_FAMILIES + value: ipv4,ipv6 --- apiVersion: v1