Skip to content

Commit 8d6a724

Browse files
authored
Merge pull request #99 from Charliekenney23/fix-generic-api-error-handling
Fix bug that caused generic API errors via InstanceExistsByProviderID to delete nodes
2 parents 77aef38 + b0111b2 commit 8d6a724

File tree

13 files changed

+700
-487
lines changed

13 files changed

+700
-487
lines changed

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ $(GOPATH)/bin/goimports:
1616
$(GOPATH)/bin/ginkgo:
1717
GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo
1818

19+
.PHONY: codegen
20+
codegen:
21+
go generate ./...
22+
1923
.PHONY: vet
2024
# lint the codebase
2125
vet:
@@ -29,18 +33,18 @@ fmt: vet $(GOPATH)/bin/goimports
2933

3034
.PHONY: test
3135
# we say code is not worth testing unless it's formatted
32-
test: $(GOPATH)/bin/ginkgo fmt
36+
test: $(GOPATH)/bin/ginkgo fmt codegen
3337
ginkgo -r --v --progress --trace --cover --skipPackage=test $(TEST_ARGS)
3438

3539
.PHONY: build-linux
36-
build-linux:
40+
build-linux: codegen
3741
echo "cross compiling linode-cloud-controller-manager for linux/amd64" && \
3842
GOOS=linux GOARCH=amd64 \
3943
CGO_ENABLED=0 \
4044
go build -o dist/linode-cloud-controller-manager-linux-amd64 .
4145

4246
.PHONY: build
43-
build:
47+
build: codegen
4448
echo "compiling linode-cloud-controller-manager" && \
4549
CGO_ENABLED=0 \
4650
go build -o dist/linode-cloud-controller-manager .

cloud/linode/client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package linode
2+
3+
//go:generate go run github.com/golang/mock/mockgen -destination mock_client_test.go -package linode github.com/linode/linode-cloud-controller-manager/cloud/linode LinodeClient
4+
5+
import (
6+
"context"
7+
8+
"github.com/linode/linodego"
9+
)
10+
11+
type LinodeClient interface {
12+
GetInstance(context.Context, int) (*linodego.Instance, error)
13+
ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error)
14+
GetInstanceIPAddresses(context.Context, int) (*linodego.InstanceIPAddressResponse, error)
15+
16+
CreateNodeBalancer(context.Context, linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error)
17+
GetNodeBalancer(context.Context, int) (*linodego.NodeBalancer, error)
18+
UpdateNodeBalancer(context.Context, int, linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error)
19+
DeleteNodeBalancer(context.Context, int) error
20+
ListNodeBalancers(context.Context, *linodego.ListOptions) ([]linodego.NodeBalancer, error)
21+
22+
CreateNodeBalancerConfig(context.Context, int, linodego.NodeBalancerConfigCreateOptions) (*linodego.NodeBalancerConfig, error)
23+
DeleteNodeBalancerConfig(context.Context, int, int) error
24+
ListNodeBalancerConfigs(context.Context, int, *linodego.ListOptions) ([]linodego.NodeBalancerConfig, error)
25+
RebuildNodeBalancerConfig(context.Context, int, int, linodego.NodeBalancerConfigRebuildOptions) (*linodego.NodeBalancerConfig, error)
26+
}
27+
28+
// linodego.Client implements LinodeClient
29+
var _ LinodeClient = (*linodego.Client)(nil)

cloud/linode/cloud.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var Options struct {
2727
}
2828

2929
type linodeCloud struct {
30-
client *linodego.Client
30+
client LinodeClient
3131
instances cloudprovider.Instances
3232
zones cloudprovider.Zones
3333
loadbalancers cloudprovider.LoadBalancer

cloud/linode/common.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package linode
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/linode/linodego"
10+
"github.com/pkg/errors"
11+
"k8s.io/apimachinery/pkg/types"
12+
cloudprovider "k8s.io/cloud-provider"
13+
)
14+
15+
const providerIDPrefix = "linode://"
16+
17+
type invalidProviderIDError struct {
18+
value string
19+
}
20+
21+
func (e invalidProviderIDError) Error() string {
22+
return fmt.Sprintf("invalid provider ID %q", e.value)
23+
}
24+
25+
func parseProviderID(providerID string) (int, error) {
26+
if !strings.HasPrefix(providerID, providerIDPrefix) {
27+
return 0, invalidProviderIDError{providerID}
28+
}
29+
id, err := strconv.Atoi(strings.TrimPrefix(providerID, providerIDPrefix))
30+
if err != nil {
31+
return 0, invalidProviderIDError{providerID}
32+
}
33+
return id, nil
34+
}
35+
36+
func linodeFilterListOptions(targetLabel string) *linodego.ListOptions {
37+
jsonFilter := fmt.Sprintf(`{"label":%q}`, targetLabel)
38+
return linodego.NewListOptions(0, jsonFilter)
39+
}
40+
41+
func linodeByName(ctx context.Context, client LinodeClient, nodeName types.NodeName) (*linodego.Instance, error) {
42+
linodes, err := client.ListInstances(ctx, linodeFilterListOptions(string(nodeName)))
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
if len(linodes) == 0 {
48+
return nil, cloudprovider.InstanceNotFound
49+
} else if len(linodes) > 1 {
50+
return nil, errors.New(fmt.Sprintf("Multiple instances found with name %v", nodeName))
51+
}
52+
53+
return &linodes[0], nil
54+
}
55+
56+
func linodeByID(ctx context.Context, client LinodeClient, id int) (*linodego.Instance, error) {
57+
instance, err := client.GetInstance(ctx, id)
58+
if err != nil {
59+
return nil, err
60+
}
61+
if instance == nil {
62+
return nil, fmt.Errorf("linode not found with id %v", id)
63+
}
64+
return instance, nil
65+
}

cloud/linode/common_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package linode
2+
3+
import "testing"
4+
5+
func TestParseProviderID(t *testing.T) {
6+
for _, tc := range []struct {
7+
name string
8+
providerID string
9+
expectedID int
10+
errExpected bool
11+
}{
12+
{
13+
name: "empty string is invalid",
14+
providerID: "",
15+
errExpected: true,
16+
},
17+
{
18+
name: "malformed provider id",
19+
providerID: "invalidproviderid!",
20+
errExpected: true,
21+
},
22+
{
23+
name: "wrong prefix",
24+
providerID: "notlinode://123",
25+
errExpected: true,
26+
},
27+
{
28+
name: "valid",
29+
providerID: "linode://123",
30+
expectedID: 123,
31+
},
32+
} {
33+
t.Run(tc.name, func(t *testing.T) {
34+
id, err := parseProviderID(tc.providerID)
35+
if err != nil {
36+
if !tc.errExpected {
37+
t.Errorf("unexpected error: %v", err)
38+
}
39+
} else if tc.errExpected {
40+
t.Error("expected an error; got nil")
41+
}
42+
43+
if id != tc.expectedID {
44+
t.Errorf("expected id to be %d; got %d", tc.expectedID, id)
45+
}
46+
})
47+
}
48+
}
49+
50+
func stringPtr(s string) *string {
51+
return &s
52+
}

cloud/linode/fake_linode_test.go

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ import (
1818
)
1919

2020
type fakeAPI struct {
21-
t *testing.T
22-
instance *linodego.Instance
23-
ips []*linodego.InstanceIP
24-
nb map[string]*linodego.NodeBalancer
25-
nbc map[string]*linodego.NodeBalancerConfig
26-
nbn map[string]*linodego.NodeBalancerNode
21+
t *testing.T
22+
nb map[string]*linodego.NodeBalancer
23+
nbc map[string]*linodego.NodeBalancerConfig
24+
nbn map[string]*linodego.NodeBalancerNode
2725

2826
requests map[fakeRequest]struct{}
2927
}
@@ -40,42 +38,8 @@ type filterStruct struct {
4038
}
4139

4240
func newFake(t *testing.T) *fakeAPI {
43-
publicIP := net.ParseIP("45.79.101.25")
44-
privateIP := net.ParseIP("192.168.133.65")
45-
instanceName := "test-instance"
46-
region := "us-east"
4741
return &fakeAPI{
48-
t: t,
49-
instance: &linodego.Instance{
50-
Label: instanceName,
51-
Region: region,
52-
Image: "linode/ubuntu16.04lts",
53-
Type: "g6-standard-2",
54-
Group: "Linode-Group",
55-
ID: 123,
56-
Status: "running",
57-
Hypervisor: "kvm",
58-
IPv4: []*net.IP{
59-
&publicIP,
60-
&privateIP,
61-
},
62-
},
63-
ips: []*linodego.InstanceIP{
64-
{
65-
Address: publicIP.String(),
66-
Public: true,
67-
LinodeID: 123,
68-
Type: "ipv4",
69-
Region: region,
70-
},
71-
{
72-
Address: privateIP.String(),
73-
Public: false,
74-
LinodeID: 123,
75-
Type: "ipv4",
76-
Region: region,
77-
},
78-
},
42+
t: t,
7943
nb: make(map[string]*linodego.NodeBalancer),
8044
nbc: make(map[string]*linodego.NodeBalancerConfig),
8145
nbn: make(map[string]*linodego.NodeBalancerNode),
@@ -112,62 +76,6 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
11276
case "GET":
11377
whichAPI := strings.Split(urlPath[1:], "/")
11478
switch whichAPI[0] {
115-
case "linode":
116-
switch whichAPI[1] {
117-
case "instances":
118-
rx, _ := regexp.Compile("/linode/instances/[0-9]+/ips")
119-
if rx.MatchString(urlPath) {
120-
resp := linodego.InstanceIPAddressResponse{
121-
IPv4: &linodego.InstanceIPv4Response{
122-
Public: []*linodego.InstanceIP{f.ips[0]},
123-
Private: []*linodego.InstanceIP{f.ips[1]},
124-
},
125-
}
126-
rr, _ := json.Marshal(resp)
127-
_, _ = w.Write(rr)
128-
return
129-
}
130-
131-
rx, _ = regexp.Compile("/linode/instances/[0-9]+")
132-
if rx.MatchString(urlPath) {
133-
id := filepath.Base(urlPath)
134-
if id == strconv.Itoa(f.instance.ID) {
135-
rr, _ := json.Marshal(&f.instance)
136-
_, _ = w.Write(rr)
137-
}
138-
return
139-
}
140-
141-
rx, _ = regexp.Compile("/linode/instances")
142-
if rx.MatchString(urlPath) {
143-
res := 0
144-
data := []linodego.Instance{}
145-
filter := r.Header.Get("X-Filter")
146-
if filter == "" {
147-
data = append(data, *f.instance)
148-
} else {
149-
var fs filterStruct
150-
err := json.Unmarshal([]byte(filter), &fs)
151-
if err != nil {
152-
f.t.Fatal(err)
153-
}
154-
if fs.Label == f.instance.Label {
155-
data = append(data, *f.instance)
156-
}
157-
}
158-
resp := linodego.InstancesPagedResponse{
159-
PageOptions: &linodego.PageOptions{
160-
Page: 1,
161-
Pages: 1,
162-
Results: res,
163-
},
164-
Data: data,
165-
}
166-
rr, _ := json.Marshal(resp)
167-
_, _ = w.Write(rr)
168-
return
169-
}
170-
}
17179
case "nodebalancers":
17280
rx, _ := regexp.Compile("/nodebalancers/[0-9]+/configs/[0-9]+/nodes/[0-9]+")
17381
if rx.MatchString(urlPath) {

0 commit comments

Comments
 (0)