Skip to content

Commit 81a6bb0

Browse files
authored
Merge pull request #139 from Charliekenney23/feat/waitForLKEClusterReady
implement WaitForLKEClusterReady
2 parents 7f61434 + 0554bee commit 81a6bb0

File tree

14 files changed

+937
-40
lines changed

14 files changed

+937
-40
lines changed

client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ func (c *Client) SetRetries() *Client {
169169
c.
170170
addRetryConditional(linodeBusyRetryCondition).
171171
addRetryConditional(tooManyRequestsRetryCondition).
172+
addRetryConditional(serviceUnavailableRetryCondition).
172173
SetRetryMaxWaitTime(APIRetryMaxWaitTime)
173174
configureRetries(c)
174175
return c

go.mod

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ module github.com/linode/linodego
33
require (
44
github.com/dnaeon/go-vcr v1.0.1
55
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48
6-
github.com/golang/protobuf v1.2.0 // indirect
76
github.com/google/go-cmp v0.4.0
8-
github.com/kr/pretty v0.1.0 // indirect
9-
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
10-
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
11-
google.golang.org/appengine v1.1.0 // indirect
12-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
13-
gopkg.in/yaml.v2 v2.2.1 // indirect
7+
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
8+
k8s.io/api v0.17.5
9+
k8s.io/apimachinery v0.17.5
10+
k8s.io/client-go v0.17.5
1411
)
1512

1613
go 1.13

go.sum

Lines changed: 169 additions & 7 deletions
Large diffs are not rendered by default.

internal/kubernetes/kubernetes.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package kubernetes
2+
3+
import (
4+
"fmt"
5+
6+
"k8s.io/client-go/kubernetes"
7+
"k8s.io/client-go/tools/clientcmd"
8+
"k8s.io/client-go/transport"
9+
)
10+
11+
// Clientset is an alias to k8s.io/client-go/kubernetes.Interface
12+
type Clientset kubernetes.Interface
13+
14+
// NewClientsetFromBytes builds a Clientset from a given Kubeconfig.
15+
//
16+
// Takes an optional transport.WrapperFunc to add request/response middleware to
17+
// api-server requests.
18+
func BuildClientsetFromConfig(
19+
kubeconfigBytes []byte,
20+
transportWrapper transport.WrapperFunc,
21+
) (Clientset, error) {
22+
config, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to parse LKE cluster kubeconfig: %s", err)
25+
}
26+
27+
restClientConfig, err := config.ClientConfig()
28+
if err != nil {
29+
return nil, fmt.Errorf("failed to get REST client config: %s", err)
30+
}
31+
32+
if transportWrapper != nil {
33+
restClientConfig.Wrap(transportWrapper)
34+
}
35+
36+
clientset, err := kubernetes.NewForConfig(restClientConfig)
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to build k8s client from LKE cluster kubeconfig: %s", err)
39+
}
40+
return clientset, nil
41+
}

lke_clusters.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,23 @@ const (
2020

2121
// LKECluster represents a LKECluster object
2222
type LKECluster struct {
23-
ID int `json:"id"`
24-
Created *time.Time `json:"-"`
25-
Updated *time.Time `json:"-"`
26-
Label string `json:"label"`
27-
Region string `json:"region"`
28-
Status LKEClusterStatus `json:"status"`
29-
Version string `json:"version"`
30-
Tags []string `json:"tags"`
23+
ID int `json:"id"`
24+
Created *time.Time `json:"-"`
25+
Updated *time.Time `json:"-"`
26+
Label string `json:"label"`
27+
Region string `json:"region"`
28+
Status LKEClusterStatus `json:"status"`
29+
K8sVersion string `json:"k8s_version"`
30+
Tags []string `json:"tags"`
3131
}
3232

3333
// LKEClusterCreateOptions fields are those accepted by CreateLKECluster
3434
type LKEClusterCreateOptions struct {
35-
NodePools []LKEClusterPoolCreateOptions `json:"node_pools"`
36-
Label string `json:"label"`
37-
Region string `json:"region"`
38-
Version string `json:"version"`
39-
Tags []string `json:"tags,omitempty"`
35+
NodePools []LKEClusterPoolCreateOptions `json:"node_pools"`
36+
Label string `json:"label"`
37+
Region string `json:"region"`
38+
K8sVersion string `json:"k8s_version"`
39+
Tags []string `json:"tags,omitempty"`
4040
}
4141

4242
// LKEClusterUpdateOptions fields are those accepted by UpdateLKECluster
@@ -85,7 +85,7 @@ func (i *LKECluster) UnmarshalJSON(b []byte) error {
8585
func (i LKECluster) GetCreateOptions() (o LKEClusterCreateOptions) {
8686
o.Label = i.Label
8787
o.Region = i.Region
88-
o.Version = i.Version
88+
o.K8sVersion = i.K8sVersion
8989
o.Tags = i.Tags
9090
// @TODO copy NodePools?
9191
return

pkg/condition/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package condition provides strategies for waiting for infrastructure
2+
// to reach a desired state through conditional predicates.
3+
package condition

pkg/condition/lke.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package condition
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/linode/linodego/internal/kubernetes"
9+
corev1 "k8s.io/api/core/v1"
10+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
)
12+
13+
// ClusterConditionFunc represents a function that tests a condition against an LKE cluster,
14+
// returns true if the condition has been reached, false if it has not yet been reached.
15+
type ClusterConditionFunc func(context.Context, kubernetes.Clientset) (bool, error)
16+
17+
// ClusterHasReadyNode is a ClusterConditionFunc which polls for at least one node to have the
18+
// condition NodeReady=True.
19+
func ClusterHasReadyNode(ctx context.Context, clientset kubernetes.Clientset) (bool, error) {
20+
nodes, err := clientset.CoreV1().Nodes().List(v1.ListOptions{})
21+
if err != nil {
22+
return false, fmt.Errorf("failed to get nodes for cluster: %s", err)
23+
}
24+
25+
for _, node := range nodes.Items {
26+
for _, condition := range node.Status.Conditions {
27+
if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
28+
return true, nil
29+
}
30+
}
31+
}
32+
33+
return false, errors.New("no nodes in cluster are ready")
34+
}

retries.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/go-resty/resty/v2"
1010
)
1111

12+
const retryAfterHeaderName = "Retry-After"
13+
1214
// type RetryConditional func(r *resty.Response) (shouldRetry bool)
1315
type RetryConditional resty.RetryConditionFunc
1416

@@ -48,8 +50,12 @@ func tooManyRequestsRetryCondition(r *resty.Response, _ error) bool {
4850
return r.StatusCode() == http.StatusTooManyRequests
4951
}
5052

53+
func serviceUnavailableRetryCondition(r *resty.Response, _ error) bool {
54+
return r.StatusCode() == http.StatusServiceUnavailable
55+
}
56+
5157
func respectRetryAfter(client *resty.Client, resp *resty.Response) (time.Duration, error) {
52-
retryAfterStr := resp.Header().Get("Retry-After")
58+
retryAfterStr := resp.Header().Get(retryAfterHeaderName)
5359
if retryAfterStr == "" {
5460
return 0, nil
5561
}

retries_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package linodego
33
import (
44
"net/http"
55
"testing"
6+
"time"
67

78
"github.com/go-resty/resty/v2"
89
)
@@ -25,7 +26,7 @@ func TestLinodeBusyRetryCondition(t *testing.T) {
2526

2627
apiError := APIError{
2728
Errors: []APIErrorReason{
28-
APIErrorReason{Reason: "Linode busy."},
29+
{Reason: "Linode busy."},
2930
},
3031
}
3132
request.SetError(&apiError)
@@ -36,3 +37,24 @@ func TestLinodeBusyRetryCondition(t *testing.T) {
3637
t.Errorf("Should have retried")
3738
}
3839
}
40+
41+
func TestLinodeServiceUnavailableRetryCondition(t *testing.T) {
42+
request := resty.Request{}
43+
rawResponse := http.Response{StatusCode: http.StatusServiceUnavailable, Header: http.Header{
44+
retryAfterHeaderName: []string{"20"},
45+
}}
46+
response := resty.Response{
47+
Request: &request,
48+
RawResponse: &rawResponse,
49+
}
50+
51+
if retry := serviceUnavailableRetryCondition(&response, nil); !retry {
52+
t.Error("expected request to be retried")
53+
}
54+
55+
if retryAfter, err := respectRetryAfter(NewClient(nil).resty, &response); err != nil {
56+
t.Errorf("expected error to be nil but got %s", err)
57+
} else if retryAfter != time.Second*20 {
58+
t.Errorf("expected retryAfter to be 20 but got %d", retryAfter)
59+
}
60+
}

0 commit comments

Comments
 (0)