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

implement WaitForLKEClusterReady #139

Merged
merged 11 commits into from
Apr 29, 2020
1 change: 1 addition & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func (c *Client) SetRetries() *Client {
c.
addRetryConditional(linodeBusyRetryCondition).
addRetryConditional(tooManyRequestsRetryCondition).
addRetryConditional(serviceUnavailableRetryCondition).
SetRetryMaxWaitTime(APIRetryMaxWaitTime)
configureRetries(c)
return c
Expand Down
11 changes: 4 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ module github.com/linode/linodego
require (
github.com/dnaeon/go-vcr v1.0.1
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48
github.com/golang/protobuf v1.2.0 // indirect
github.com/google/go-cmp v0.4.0
github.com/kr/pretty v0.1.0 // indirect
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
google.golang.org/appengine v1.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
k8s.io/api v0.17.5
k8s.io/apimachinery v0.17.5
k8s.io/client-go v0.17.5
)

go 1.13
176 changes: 169 additions & 7 deletions go.sum

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions internal/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kubernetes

import (
"fmt"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/transport"
)

// Clientset is an alias to k8s.io/client-go/kubernetes.Interface
type Clientset kubernetes.Interface

// NewClientsetFromBytes builds a Clientset from a given Kubeconfig.
//
// Takes an optional transport.WrapperFunc to add request/response middleware to
// api-server requests.
func BuildClientsetFromConfig(
kubeconfigBytes []byte,
transportWrapper transport.WrapperFunc,
) (Clientset, error) {
config, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse LKE cluster kubeconfig: %s", err)
}

restClientConfig, err := config.ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to get REST client config: %s", err)
}

if transportWrapper != nil {
restClientConfig.Wrap(transportWrapper)
}

clientset, err := kubernetes.NewForConfig(restClientConfig)
if err != nil {
return nil, fmt.Errorf("failed to build k8s client from LKE cluster kubeconfig: %s", err)
}
return clientset, nil
}
28 changes: 14 additions & 14 deletions lke_clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@ const (

// LKECluster represents a LKECluster object
type LKECluster struct {
ID int `json:"id"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
Label string `json:"label"`
Region string `json:"region"`
Status LKEClusterStatus `json:"status"`
Version string `json:"version"`
Tags []string `json:"tags"`
ID int `json:"id"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
Label string `json:"label"`
Region string `json:"region"`
Status LKEClusterStatus `json:"status"`
K8sVersion string `json:"k8s_version"`
Tags []string `json:"tags"`
}

// LKEClusterCreateOptions fields are those accepted by CreateLKECluster
type LKEClusterCreateOptions struct {
NodePools []LKEClusterPoolCreateOptions `json:"node_pools"`
Label string `json:"label"`
Region string `json:"region"`
Version string `json:"version"`
Tags []string `json:"tags,omitempty"`
NodePools []LKEClusterPoolCreateOptions `json:"node_pools"`
Label string `json:"label"`
Region string `json:"region"`
K8sVersion string `json:"k8s_version"`
Tags []string `json:"tags,omitempty"`
}

// LKEClusterUpdateOptions fields are those accepted by UpdateLKECluster
Expand Down Expand Up @@ -85,7 +85,7 @@ func (i *LKECluster) UnmarshalJSON(b []byte) error {
func (i LKECluster) GetCreateOptions() (o LKEClusterCreateOptions) {
o.Label = i.Label
o.Region = i.Region
o.Version = i.Version
o.K8sVersion = i.K8sVersion
o.Tags = i.Tags
// @TODO copy NodePools?
return
Expand Down
3 changes: 3 additions & 0 deletions pkg/condition/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package condition provides strategies for waiting for infrastructure
// to reach a desired state through conditional predicates.
package condition
34 changes: 34 additions & 0 deletions pkg/condition/lke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package condition

import (
"context"
"errors"
"fmt"

"github.com/linode/linodego/internal/kubernetes"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ClusterConditionFunc represents a function that tests a condition against an LKE cluster,
// returns true if the condition has been reached, false if it has not yet been reached.
type ClusterConditionFunc func(context.Context, kubernetes.Clientset) (bool, error)

// ClusterHasReadyNode is a ClusterConditionFunc which polls for at least one node to have the
// condition NodeReady=True.
func ClusterHasReadyNode(ctx context.Context, clientset kubernetes.Clientset) (bool, error) {
nodes, err := clientset.CoreV1().Nodes().List(v1.ListOptions{})
if err != nil {
return false, fmt.Errorf("failed to get nodes for cluster: %s", err)
}

for _, node := range nodes.Items {
for _, condition := range node.Status.Conditions {
if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
return true, nil
}
}
}

return false, errors.New("no nodes in cluster are ready")
}
8 changes: 7 additions & 1 deletion retries.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/go-resty/resty/v2"
)

const retryAfterHeaderName = "Retry-After"

// type RetryConditional func(r *resty.Response) (shouldRetry bool)
type RetryConditional resty.RetryConditionFunc

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

func serviceUnavailableRetryCondition(r *resty.Response, _ error) bool {
return r.StatusCode() == http.StatusServiceUnavailable
}

func respectRetryAfter(client *resty.Client, resp *resty.Response) (time.Duration, error) {
retryAfterStr := resp.Header().Get("Retry-After")
retryAfterStr := resp.Header().Get(retryAfterHeaderName)
if retryAfterStr == "" {
return 0, nil
}
Expand Down
24 changes: 23 additions & 1 deletion retries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package linodego
import (
"net/http"
"testing"
"time"

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

apiError := APIError{
Errors: []APIErrorReason{
APIErrorReason{Reason: "Linode busy."},
{Reason: "Linode busy."},
},
}
request.SetError(&apiError)
Expand All @@ -36,3 +37,24 @@ func TestLinodeBusyRetryCondition(t *testing.T) {
t.Errorf("Should have retried")
}
}

func TestLinodeServiceUnavailableRetryCondition(t *testing.T) {
request := resty.Request{}
rawResponse := http.Response{StatusCode: http.StatusServiceUnavailable, Header: http.Header{
retryAfterHeaderName: []string{"20"},
}}
response := resty.Response{
Request: &request,
RawResponse: &rawResponse,
}

if retry := serviceUnavailableRetryCondition(&response, nil); !retry {
t.Error("expected request to be retried")
}

if retryAfter, err := respectRetryAfter(NewClient(nil).resty, &response); err != nil {
t.Errorf("expected error to be nil but got %s", err)
} else if retryAfter != time.Second*20 {
t.Errorf("expected retryAfter to be 20 but got %d", retryAfter)
}
}
Loading