Skip to content

Commit 2fe9680

Browse files
authored
Fetch providerID from DigitalOcean API as a fallback if not specified on the node object (#842)
1 parent 5274090 commit 2fe9680

File tree

6 files changed

+119
-19
lines changed

6 files changed

+119
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
* Add optional `DEFAULT_LB_TYPE` environment variable to configure the default load balancer type between `REGIONAL` and `REGIONAL_NETWORK` (@olove)
44
* Restrict the kube-proxy health port to be accessible by the load balancer only instead of allowing all IPv4 and IPv6 addresses (@olove)
5+
* Fixes an issue with nodes that don't have their providerID set from the kubelet configuration. From now on,
6+
if the node object `node.Spec.ProviderID` property is empty, we will retrieve the information using the DO API by
7+
listing the droplets with that name.
58

69
## v0.1.61 (beta) - May 29, 2025
710
* Ensure Network LoadBalancer (NLB) is the default loadbalancer type (@olove)

cloud-controller-manager/do/common.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ import (
2929
// are very large and the response gets too big with 200 objects
3030
const apiResultsPerPage = 50
3131

32-
func allDropletList(ctx context.Context, client *godo.Client) ([]godo.Droplet, error) {
33-
list := []godo.Droplet{}
32+
func allDropletList(ctx context.Context, listFunc func(ctx context.Context, opt *godo.ListOptions) ([]godo.Droplet, *godo.Response, error)) ([]godo.Droplet, error) {
33+
var list []godo.Droplet
3434

3535
opt := &godo.ListOptions{Page: 1, PerPage: apiResultsPerPage}
3636
for {
37-
droplets, resp, err := client.Droplets.List(ctx, opt)
37+
droplets, resp, err := listFunc(ctx, opt)
38+
3839
if err != nil {
3940
return nil, err
4041
}

cloud-controller-manager/do/common_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ func TestAllDropletList(t *testing.T) {
122122
},
123123
)
124124

125-
droplets, err := allDropletList(context.Background(), client)
125+
droplets, err := allDropletList(context.Background(), func(ctx context.Context, opt *godo.ListOptions) ([]godo.Droplet, *godo.Response, error) {
126+
return client.Droplets.List(ctx, opt)
127+
})
126128
if err != nil {
127129
t.Fatalf("unexpected err: %s", err)
128130
}

cloud-controller-manager/do/droplets.go

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ import (
2626

2727
"github.com/digitalocean/godo"
2828
v1 "k8s.io/api/core/v1"
29+
"k8s.io/apimachinery/pkg/types"
2930
cloudprovider "k8s.io/cloud-provider"
3031
)
3132

3233
const (
3334
dropletShutdownStatus = "off"
35+
providerIDPrefix = "digitalocean://"
3436
)
3537

3638
// instances implements the InstancesV2() interface
@@ -92,25 +94,66 @@ func (i *instances) InstanceShutdown(ctx context.Context, node *v1.Node) (bool,
9294
return droplet.Status == dropletShutdownStatus, nil
9395
}
9496

95-
func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
96-
dropletID, err := dropletIDFromProviderID(node.Spec.ProviderID)
97+
// dropletByName returns a *godo.Droplet for the droplet identified by nodeName.
98+
//
99+
// When nodeName identifies more than one droplet, only the first will be
100+
// considered.
101+
func dropletByName(ctx context.Context, client *godo.Client, nodeName types.NodeName) (*godo.Droplet, error) {
102+
droplets, err := allDropletList(ctx, func(ctx context.Context, opt *godo.ListOptions) ([]godo.Droplet, *godo.Response, error) {
103+
return client.Droplets.ListByName(ctx, string(nodeName), opt)
104+
})
97105
if err != nil {
98-
return nil, fmt.Errorf("determining droplet ID from providerID: %s", err.Error())
106+
return nil, err
99107
}
100108

101-
droplet, err := dropletByID(ctx, i.resources.gclient, dropletID)
102-
if err != nil {
103-
return nil, fmt.Errorf("getting droplet by ID: %s: ", err.Error())
109+
for _, droplet := range droplets {
110+
if droplet.Name == string(nodeName) {
111+
return &droplet, nil
112+
}
113+
addresses, _ := nodeAddresses(&droplet)
114+
for _, address := range addresses {
115+
if address.Address == string(nodeName) {
116+
return &droplet, nil
117+
}
118+
}
104119
}
105-
if droplet == nil {
106-
return nil, fmt.Errorf("droplet %d for node %s does not exist", dropletID, node.Name)
120+
121+
return nil, cloudprovider.InstanceNotFound
122+
}
123+
124+
func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
125+
var (
126+
dropletID int
127+
err error
128+
droplet *godo.Droplet
129+
)
130+
131+
if node.Spec.ProviderID == "" {
132+
droplet, err = dropletByName(ctx, i.resources.gclient, types.NodeName(node.GetName()))
133+
if err != nil {
134+
return nil, fmt.Errorf("getting droplet by name: %s", err.Error())
135+
}
136+
dropletID = droplet.ID
137+
} else {
138+
dropletID, err = dropletIDFromProviderID(node.Spec.ProviderID)
139+
if err != nil {
140+
return nil, fmt.Errorf("determining droplet ID from providerID: %s", err.Error())
141+
}
142+
droplet, err = dropletByID(ctx, i.resources.gclient, dropletID)
143+
if err != nil {
144+
return nil, fmt.Errorf("getting droplet by ID: %s: ", err.Error())
145+
}
146+
if droplet == nil {
147+
return nil, fmt.Errorf("droplet %d for node %s does not exist", dropletID, node.Name)
148+
}
107149
}
150+
108151
nodeAddrs, err := nodeAddresses(droplet)
109152
if err != nil {
110153
return nil, fmt.Errorf("getting node addresses of droplet %d for node %s: %s", dropletID, node.Name, err.Error())
111154
}
112155
return &cloudprovider.InstanceMetadata{
113-
ProviderID: node.Spec.ProviderID, // the providerID may or may not be present according to the interface doc. However, we set this from kubelet.
156+
ProviderID: fmt.Sprintf("%s%d", providerIDPrefix, dropletID),
114157
InstanceType: droplet.SizeSlug,
115158
Region: droplet.Region.Slug,
116159
NodeAddresses: nodeAddrs,
@@ -132,13 +175,11 @@ func dropletIDFromProviderID(providerID string) (int, error) {
132175
return 0, errors.New("provider ID cannot be empty")
133176
}
134177

135-
const prefix = "digitalocean://"
136-
137-
if !strings.HasPrefix(providerID, prefix) {
138-
return 0, fmt.Errorf("provider ID %q is missing prefix %q", providerID, prefix)
178+
if !strings.HasPrefix(providerID, providerIDPrefix) {
179+
return 0, fmt.Errorf("provider ID %q is missing prefix %q", providerID, providerIDPrefix)
139180
}
140181

141-
provIDNum := strings.TrimPrefix(providerID, prefix)
182+
provIDNum := strings.TrimPrefix(providerID, providerIDPrefix)
142183
if provIDNum == "" {
143184
return 0, errors.New("provider ID number cannot be empty")
144185
}

cloud-controller-manager/do/droplets_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,57 @@ func TestInstanceMetadata(t *testing.T) {
339339
}
340340
}
341341

342+
func TestInstanceMetadataWithoutProviderID(t *testing.T) {
343+
fake := &fakeDropletService{}
344+
fake.listByNameFunc = func(ctx context.Context, name string, opt *godo.ListOptions) ([]godo.Droplet, *godo.Response, error) {
345+
droplet := newFakeDroplet()
346+
resp := newFakeOKResponse()
347+
return []godo.Droplet{*droplet}, resp, nil
348+
}
349+
res := &resources{gclient: newFakeDropletClient(fake)}
350+
instances := newInstances(res, "nyc1")
351+
352+
expectedAddresses := []v1.NodeAddress{
353+
{
354+
Type: v1.NodeHostName,
355+
Address: "test-droplet",
356+
},
357+
{
358+
Type: v1.NodeInternalIP,
359+
Address: "10.0.0.0",
360+
},
361+
{
362+
Type: v1.NodeExternalIP,
363+
Address: "99.99.99.99",
364+
},
365+
}
366+
367+
metadata, err := instances.InstanceMetadata(context.TODO(), &v1.Node{
368+
ObjectMeta: metav1.ObjectMeta{
369+
Name: "test-droplet",
370+
},
371+
Spec: v1.NodeSpec{},
372+
})
373+
if err != nil {
374+
t.Errorf("unexpected err, expected nil. got: %v", err)
375+
}
376+
377+
if !reflect.DeepEqual(metadata.NodeAddresses, expectedAddresses) {
378+
t.Errorf("unexpected node addresses. got: %v want: %v", metadata.NodeAddresses, expectedAddresses)
379+
}
380+
381+
if metadata.ProviderID != "digitalocean://123" {
382+
t.Errorf("unexpected node providerID. got: %v, want: %v", metadata.ProviderID, "digitalocean://123")
383+
}
384+
385+
if metadata.InstanceType != "2gb" {
386+
t.Errorf("unexpected node instance type. got: %v want: %v", metadata.InstanceType, newFakeDroplet().SizeSlug)
387+
}
388+
if metadata.Region != "test1" {
389+
t.Errorf("unexpected node region. got: %v want: %v", metadata.Region, newFakeDroplet().Region)
390+
}
391+
}
392+
342393
func Test_dropletIDFromProviderID(t *testing.T) {
343394
testcases := []struct {
344395
name string

cloud-controller-manager/do/loadbalancers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,9 @@ func (l *loadBalancers) nodesToDropletIDs(ctx context.Context, nodes []*v1.Node)
533533

534534
if len(missingDroplets) > 0 {
535535
// Discover missing droplets by matching names.
536-
droplets, err := allDropletList(ctx, l.resources.gclient)
536+
droplets, err := allDropletList(ctx, func(ctx context.Context, opt *godo.ListOptions) ([]godo.Droplet, *godo.Response, error) {
537+
return l.resources.gclient.Droplets.List(ctx, opt)
538+
})
537539
if err != nil {
538540
return nil, fmt.Errorf("failed to list all droplets: %s", err)
539541
}

0 commit comments

Comments
 (0)