Skip to content

Commit

Permalink
chore(kubernetes): add WaitForKubernetesNodeGroupState method
Browse files Browse the repository at this point in the history
  • Loading branch information
villevsv-upcloud committed Oct 24, 2023
1 parent 5080211 commit d96f2b0
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 16 deletions.
13 changes: 13 additions & 0 deletions upcloud/request/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ func (r *WaitForKubernetesClusterStateRequest) RequestURL() string {
return fmt.Sprintf("%s/%s", kubernetesClusterBasePath, r.UUID)
}

// WaitForKubernetesNodeGroupStateRequest represents a request to wait for a Kubernetes node group
// to enter a desired state
type WaitForKubernetesNodeGroupStateRequest struct {
DesiredState upcloud.KubernetesNodeGroupState `json:"-"`
Timeout time.Duration `json:"-"`
ClusterUUID string `json:"-"`
Name string `json:"-"`
}

func (r *WaitForKubernetesNodeGroupStateRequest) RequestURL() string {
return fmt.Sprintf("%s/%s/node-groups/%s", kubernetesClusterBasePath, r.ClusterUUID, r.Name)
}

// GetKubernetesKubeconfigRequest represents a request to get kubeconfig for a Kubernetes cluster
type GetKubernetesKubeconfigRequest struct {
UUID string `json:"-"`
Expand Down
41 changes: 39 additions & 2 deletions upcloud/service/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Kubernetes interface {
GetKubernetesNodeGroup(ctx context.Context, r *request.GetKubernetesNodeGroupRequest) (*upcloud.KubernetesNodeGroupDetails, error)
CreateKubernetesNodeGroup(ctx context.Context, r *request.CreateKubernetesNodeGroupRequest) (*upcloud.KubernetesNodeGroup, error)
ModifyKubernetesNodeGroup(ctx context.Context, r *request.ModifyKubernetesNodeGroupRequest) (*upcloud.KubernetesNodeGroup, error)
WaitForKubernetesNodeGroupState(ctx context.Context, r *request.WaitForKubernetesNodeGroupStateRequest) (*upcloud.KubernetesNodeGroupDetails, error)
DeleteKubernetesNodeGroup(ctx context.Context, r *request.DeleteKubernetesNodeGroupRequest) error
DeleteKubernetesNodeGroupNode(ctx context.Context, r *request.DeleteKubernetesNodeGroupNodeRequest) error
GetKubernetesPlans(ctx context.Context, r *request.GetKubernetesPlansRequest) ([]upcloud.KubernetesPlan, error)
Expand Down Expand Up @@ -72,8 +73,8 @@ func (s *Service) DeleteKubernetesCluster(ctx context.Context, r *request.Delete
return s.delete(ctx, r)
}

// WaitForKubernetesClusterState (EXPERIMENTAL) blocks execution until the specified Kubernetes cluster has entered the
// specified state. If the state changes favorably, cluster details is returned. The method will give up
// WaitForKubernetesClusterState blocks execution until the specified Kubernetes cluster has entered the
// specified state. If the state changes favorably, cluster details are returned. The method will give up
// after the specified timeout
func (s *Service) WaitForKubernetesClusterState(ctx context.Context, r *request.WaitForKubernetesClusterStateRequest) (*upcloud.KubernetesCluster, error) {
attempts := 0
Expand Down Expand Up @@ -107,6 +108,42 @@ func (s *Service) WaitForKubernetesClusterState(ctx context.Context, r *request.
}
}

// WaitForKubernetesNodeGroupState blocks execution until the specified Kubernetes node group has entered the
// specified state. If the state changes favorably, node group details are returned. The method will give up
// after the specified timeout
func (s *Service) WaitForKubernetesNodeGroupState(ctx context.Context, r *request.WaitForKubernetesNodeGroupStateRequest) (*upcloud.KubernetesNodeGroupDetails, error) {
attempts := 0
sleepDuration := time.Second * 5

for {
attempts++

details, err := s.GetKubernetesNodeGroup(ctx, &request.GetKubernetesNodeGroupRequest{
ClusterUUID: r.ClusterUUID,
Name: r.Name,
})
if err != nil {
// Ignore first two 404 responses to avoid errors caused by possible false NOT_FOUND responses right after cluster has been created.
var ucErr *upcloud.Problem
if errors.As(err, &ucErr) && ucErr.Status == http.StatusNotFound && attempts < 3 {
log.Printf("ERROR: %+v", err)
} else {
return nil, err
}
}

if details.State == r.DesiredState {
return details, nil
}

time.Sleep(sleepDuration)

if time.Duration(attempts)*sleepDuration >= r.Timeout {
return nil, fmt.Errorf("timeout reached while waiting for Kubernetes node group to enter state \"%s\"", r.DesiredState)
}
}
}

// GetKubernetesKubeconfig retrieves kubeconfig of a Kubernetes cluster.
func (s *Service) GetKubernetesKubeconfig(ctx context.Context, r *request.GetKubernetesKubeconfigRequest) (string, error) {
// TODO: should timeout be part of GetKubernetesKubeconfigRequest ?
Expand Down
103 changes: 89 additions & 14 deletions upcloud/service/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func TestGetKubernetesClusters(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes", client.APIVersion), r.URL.Path)
fmt.Fprintf(w, "[%s]", exampleClusterResponse)
_, _ = fmt.Fprintf(w, "[%s]", exampleClusterResponse)
}))
defer srv.Close()
res, err := svc.GetKubernetesClusters(context.Background(), &request.GetKubernetesClustersRequest{})
Expand Down Expand Up @@ -197,7 +197,7 @@ func TestGetKubernetesClusterDetails(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_", client.APIVersion), r.URL.Path)
fmt.Fprint(w, exampleClusterResponse)
_, _ = fmt.Fprint(w, exampleClusterResponse)
}))
defer srv.Close()
res, err := svc.GetKubernetesCluster(context.Background(), &request.GetKubernetesClusterRequest{UUID: "_UUID_"})
Expand All @@ -216,7 +216,7 @@ func TestCreateKubernetesCluster(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CreateKubernetesCluster method first makes a request to /network/:uuid to check network CIDR
if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/%s/network/03e4970d-7791-4b80-a892-682ae0faf46b", client.APIVersion) {
fmt.Fprint(w, exampleNetworkResponse)
_, _ = fmt.Fprint(w, exampleNetworkResponse)
return
}

Expand All @@ -225,7 +225,7 @@ func TestCreateKubernetesCluster(t *testing.T) {
err := json.NewDecoder(r.Body).Decode(&payload)
assert.NoError(t, err)

fmt.Fprint(w, exampleClusterResponse)
_, _ = fmt.Fprint(w, exampleClusterResponse)
return
}

Expand Down Expand Up @@ -307,7 +307,7 @@ func TestGetKubernetesNodeGroups(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups", client.APIVersion), r.URL.Path)
fmt.Fprintf(w, "[%s]", exampleNodeGroupResponse)
_, _ = fmt.Fprintf(w, "[%s]", exampleNodeGroupResponse)
}))
defer srv.Close()

Expand All @@ -324,7 +324,7 @@ func TestGetKubernetesNodeGroup(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups/_NAME_", client.APIVersion), r.URL.Path)
fmt.Fprint(w, exampleNodeGroupResponse)
_, _ = fmt.Fprint(w, exampleNodeGroupResponse)
}))
defer srv.Close()

Expand All @@ -342,7 +342,7 @@ func TestGetKubernetesNodeGroupDetails(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups/_NAME_", client.APIVersion), r.URL.Path)
fmt.Fprint(w, exampleNodeGroupDetailsResponse)
_, _ = fmt.Fprint(w, exampleNodeGroupDetailsResponse)
}))
defer srv.Close()

Expand All @@ -368,7 +368,7 @@ func TestCreateKubernetesNodeGroup(t *testing.T) {
err := json.NewDecoder(r.Body).Decode(&payload)
assert.NoError(t, err)

fmt.Fprint(w, exampleNodeGroupResponse)
_, _ = fmt.Fprint(w, exampleNodeGroupResponse)
}))
defer srv.Close()

Expand Down Expand Up @@ -421,7 +421,7 @@ func TestModifyKubernetesNodeGroup(t *testing.T) {
err := json.NewDecoder(r.Body).Decode(&payload)
assert.NoError(t, err)

fmt.Fprint(w, exampleNodeGroupResponse)
_, _ = fmt.Fprint(w, exampleNodeGroupResponse)
}))
defer srv.Close()

Expand All @@ -440,7 +440,7 @@ func TestDeleteKubernetesNodeGroup(t *testing.T) {
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups/_NAME_", client.APIVersion), r.URL.Path)
fmt.Fprint(w, exampleNodeGroupResponse)
_, _ = fmt.Fprint(w, exampleNodeGroupResponse)
}))
defer srv.Close()

Expand Down Expand Up @@ -480,7 +480,7 @@ func TestWaitForKubernetesClusterState(t *testing.T) {
requestsMade++

if requestsCounter >= 2 {
fmt.Fprint(w, `
_, _ = fmt.Fprint(w, `
{
"name":"test-name",
"network":"03e4970d-7791-4b80-a892-682ae0faf46b",
Expand All @@ -493,7 +493,7 @@ func TestWaitForKubernetesClusterState(t *testing.T) {
`)
} else {
requestsCounter++
fmt.Fprint(w, `
_, _ = fmt.Fprint(w, `
{
"name":"test-name",
"network":"03e4970d-7791-4b80-a892-682ae0faf46b",
Expand All @@ -517,19 +517,94 @@ func TestWaitForKubernetesClusterState(t *testing.T) {
assert.Equal(t, 3, requestsMade)
}

func TestWaitForKubernetesNodeGroupState(t *testing.T) {
t.Parallel()

requestsCounter := 0
requestsMade := 0

srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups/_NAME_", client.APIVersion), r.URL.Path)

requestsMade++

if requestsCounter >= 2 {
_, _ = fmt.Fprint(w, `
{
"anti_affinity": false,
"count": 1,
"kubelet_args": [],
"labels": [],
"name": "test-name",
"nodes": [
{
"name": "test-name-p6w9h",
"state": "running",
"uuid": "107b7750-752a-4b71-a4c5-025198178e09"
}
],
"plan": "1xCPU-1GB",
"ssh_keys": [
"test-key"
],
"state": "running",
"storage": "01000000-0000-4000-8000-000160020100",
"utility_network_access": false
}
`)
} else {
requestsCounter++
_, _ = fmt.Fprint(w, `
{
"anti_affinity": false,
"count": 1,
"kubelet_args": [],
"labels": [],
"name": "test-name",
"nodes": [
{
"name": "test-name-p6w9h",
"state": "running",
"uuid": "107b7750-752a-4b71-a4c5-025198178e09"
}
],
"plan": "1xCPU-1GB",
"ssh_keys": [
"test-key"
],
"state": "scaling-up",
"storage": "01000000-0000-4000-8000-000160020100",
"utility_network_access": false
}
`)
}
}))
defer srv.Close()

_, err := svc.WaitForKubernetesNodeGroupState(context.Background(), &request.WaitForKubernetesNodeGroupStateRequest{
ClusterUUID: "_UUID_",
DesiredState: upcloud.KubernetesNodeGroupStateRunning,
Timeout: time.Second * 20,
Name: "_NAME_",
})
assert.NoError(t, err)
assert.Equal(t, 3, requestsMade)
}

func TestGetKubernetesKubeconfig(t *testing.T) {
t.Parallel()

srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetKubernetesKubeconfig first fetches cluster details to check for running state, so we must
// take care of both requests
if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/%s/kubernetes/_UUID_", client.APIVersion) {
fmt.Fprint(w, exampleClusterResponse)
_, _ = fmt.Fprint(w, exampleClusterResponse)
return
}

if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/%s/kubernetes/_UUID_/kubeconfig", client.APIVersion) {
fmt.Fprint(w, exampleKubeconfigResponse)
_, _ = fmt.Fprint(w, exampleKubeconfigResponse)
return
}

Expand Down

0 comments on commit d96f2b0

Please sign in to comment.