Skip to content
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

Handle correct providerID format #36

Merged
merged 1 commit into from Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 34 additions & 3 deletions do/droplets.go
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"

"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -54,8 +55,12 @@ func (i *instances) NodeAddresses(nodeName types.NodeName) ([]v1.NodeAddress, er
// NodeAddressesByProviderID returns all the valid addresses of the specified
// node by providerId. For DO this is the public/private ipv4 addresses for now.
func (i *instances) NodeAddressesByProviderID(providerId string) ([]v1.NodeAddress, error) {
// we can technically get all the required data from metadata service
droplet, err := dropletByID(context.TODO(), i.client, providerId)
id, err := dropletIDFromProviderID(providerId)
if err != nil {
return nil, err
}

droplet, err := dropletByID(context.TODO(), i.client, id)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -111,7 +116,12 @@ func (i *instances) InstanceType(name types.NodeName) (string, error) {

// InstanceTypeByProviderID returns the type of the specified instance.
func (i *instances) InstanceTypeByProviderID(providerId string) (string, error) {
droplet, err := dropletByID(context.TODO(), i.client, providerId)
id, err := dropletIDFromProviderID(providerId)
if err != nil {
return "", err
}

droplet, err := dropletByID(context.TODO(), i.client, id)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -174,3 +184,24 @@ func dropletByName(ctx context.Context, client *godo.Client, nodeName types.Node

return nil, cloudprovider.InstanceNotFound
}

// dropletIDFromProviderID returns a droplet's ID extracted from the node's
// providerID spec. The providerID spec should be retrievable from the Kubernetes
// node object. The expected format is: digitalocean://droplet-id
func dropletIDFromProviderID(providerID string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some comments below that are just nitpicks. Take them or not.

Also consider: is there any reason to split on the slashes at all? Why not just

func dropletIDFromProviderID(providerID string) (string, error) {
    const scheme := providerID + "://"

    if !strings.HasPrefix(providerID, scheme) != 0 {
        return "", fmt.Errorf("unexpected providerID format: %s, format should be: digitalocean://12345", providerID)
    }
    return strings.TrimPrefix(providerID, scheme), nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean

const scheme := providerName + "://"

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here I ended up doing strings.Split(providerID, "/") since it makes validation stricter. For example if the provider ID happened to be digitalocean:////droplet-id (which I have seen before on AWS clusters) then trim prefix wouldn't work here. Seems unlikely but possible? The problem mainly is that CCM doesn't have control over what the node provider ID is set to, we can only hope for the best and assume it's a format we expect, otherwise we actually call the *ByNodeName methods instead.

Copy link
Contributor

@bhcleek bhcleek Sep 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great reason for using splitN or HasPrefix, though. Right now on master, instances.NodeAddressesByProviderID will look for the providerID given. All this PR is about is making sure that digitalocean:// is a prefix of providerID. The PR, though, changes behavior where previously instances.dropletById would have been given //droplet-id.

Unless we're prepared to validate other rules about the droplet-id value, not allowing leading /s seems out of scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having said all that, this is all a nitpick; I don't intend it to be a blocker.

if providerID == "" {
return "", errors.New("providerID cannot be empty string")
}

split := strings.Split(providerID, "/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.SplitN(providerID, "/", 3)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as above

if len(split) != 3 {
return "", fmt.Errorf("unexpected providerID format: %s, format should be: digitalocean://12345", providerID)
}

// since split[0] is actually "digitalocean:"
if strings.TrimSuffix(split[0], ":") != providerName {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just if split[0] != providerName + ":" {?

return "", fmt.Errorf("provider name from providerID should be digitalocean: %s", providerID)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return "", fmt.Errorf("provider name from providerID should be %s://%s", providerName, providerID)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

providerID is already in the format providerName://id so it would be digitalocean://digitalocean://droplet-id. I think the confusion here is that providerID is the droplet ID which it is not, it's a separate concept from Kubernetes land :P


return split[2], nil
}
55 changes: 54 additions & 1 deletion do/droplets_test.go
Expand Up @@ -18,6 +18,7 @@ package do

import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"reflect"
Expand Down Expand Up @@ -205,7 +206,7 @@ func TestNodeAddressesByProviderID(t *testing.T) {
},
}

addresses, err := instances.NodeAddressesByProviderID("123")
addresses, err := instances.NodeAddressesByProviderID("digitalocean://123")

if !reflect.DeepEqual(addresses, expectedAddresses) {
t.Errorf("unexpected node addresses. got: %v want: %v", addresses, expectedAddresses)
Expand Down Expand Up @@ -261,3 +262,55 @@ func TestInstanceType(t *testing.T) {
t.Errorf("expected type 2gb, got: %s", instanceType)
}
}

func Test_dropletIDFromProviderID(t *testing.T) {
testcases := []struct {
name string
providerID string
dropletID string
err error
}{
{
"valid providerID",
"digitalocean://12345",
"12345",
nil,
},
{
"invalid providerID - empty string",
"",
"",
errors.New("providerID cannot be empty string"),
},
{
"invalid providerID - wrong format",
"digitalocean:/12345",
"",
errors.New("unexpected providerID format: digitalocean:/12345, format should be: digitalocean://12345"),
},
{
"invalid providerID - wrong provider name",
"do://12345",
"",
errors.New("provider name from providerID should be digitalocean: do://12345"),
},
}

for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
dropletID, err := dropletIDFromProviderID(testcase.providerID)
if dropletID != testcase.dropletID {
t.Errorf("actual droplet ID: %s", dropletID)
t.Errorf("expected droplet ID: %s", testcase.dropletID)
t.Error("unexpected droplet ID")
}

if !reflect.DeepEqual(err, testcase.err) {
t.Errorf("actual err: %v", err)
t.Errorf("expected err: %v", testcase.err)
t.Error("unexpected err")
}
})
}

}
7 changes: 6 additions & 1 deletion do/zones.go
Expand Up @@ -42,7 +42,12 @@ func (z zones) GetZone() (cloudprovider.Zone, error) {
// locality region of the node specified by providerId. GetZoneByProviderID
// will only fill the Region field of cloudprovider.Zone for DO.
func (z zones) GetZoneByProviderID(providerID string) (cloudprovider.Zone, error) {
d, err := dropletByID(context.Background(), z.client, providerID)
id, err := dropletIDFromProviderID(providerID)
if err != nil {
return cloudprovider.Zone{}, err
}

d, err := dropletByID(context.Background(), z.client, id)
if err != nil {
return cloudprovider.Zone{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion do/zones_test.go
Expand Up @@ -50,7 +50,7 @@ func TestZones_GetZoneByProviderID(t *testing.T) {

expected := cloudprovider.Zone{Region: "test1"}

actual, err := zones.GetZoneByProviderID("123")
actual, err := zones.GetZoneByProviderID("digitalocean://123")

if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected region. got: %+v want: %+v", actual, expected)
Expand Down