From b64f2f0f2098d95955d8f5cb214d93a562c86682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4lim=C3=A4ki?= <110451292+villevsv-upcloud@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:11:54 +0300 Subject: [PATCH] chore(kubernetes): add `WaitForKubernetesNodeGroupState` method (#272) --- CHANGELOG.md | 3 + upcloud/request/kubernetes.go | 13 +++++ upcloud/service/kubernetes.go | 41 +++++++++++++- upcloud/service/kubernetes_test.go | 89 +++++++++++++++++++++++++----- 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c23c7539..2d0f38ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +## Added +- kubernetes: `WaitForKubernetesNodeGroupState` method for waiting the node group to achieve a desired state + ## [6.8.2] ### Added - account: `NetworkPeerings`, `NTPExcessGiB`, `StorageMaxIOPS`, and `LoadBalancers` fields to the `ResourceLimits` struct. diff --git a/upcloud/request/kubernetes.go b/upcloud/request/kubernetes.go index 24bd124c..5f7077c3 100644 --- a/upcloud/request/kubernetes.go +++ b/upcloud/request/kubernetes.go @@ -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:"-"` diff --git a/upcloud/service/kubernetes.go b/upcloud/service/kubernetes.go index 10ce00c9..ef281dbd 100644 --- a/upcloud/service/kubernetes.go +++ b/upcloud/service/kubernetes.go @@ -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.KubernetesNodeGroup, 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) @@ -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 @@ -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 is returned. The method will give up +// after the specified timeout +func (s *Service) WaitForKubernetesNodeGroupState(ctx context.Context, r *request.WaitForKubernetesNodeGroupStateRequest) (*upcloud.KubernetesNodeGroup, error) { + attempts := 0 + sleepDuration := time.Second * 5 + + for { + attempts++ + + ng, 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 ng.State == r.DesiredState { + return &ng.KubernetesNodeGroup, 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 ? diff --git a/upcloud/service/kubernetes_test.go b/upcloud/service/kubernetes_test.go index ed13e5c7..b796ac11 100644 --- a/upcloud/service/kubernetes_test.go +++ b/upcloud/service/kubernetes_test.go @@ -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{}) @@ -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_"}) @@ -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 } @@ -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 } @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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", @@ -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", @@ -517,6 +517,67 @@ 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", + "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", + "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() @@ -524,12 +585,12 @@ func TestGetKubernetesKubeconfig(t *testing.T) { // 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 }