diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/azure_zones_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_zones_test.go index fac4a33f22a..ffa433dcf19 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/azure_zones_test.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_zones_test.go @@ -24,6 +24,19 @@ import ( "net" "net/http" "testing" + + "k8s.io/apimachinery/pkg/util/sets" + cloudprovider "k8s.io/cloud-provider" + "k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" + + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" + "github.com/Azure/go-autorest/autorest/to" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +const ( + testAvailabilitySetNodeProviderID = "azure:///subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm-0" ) func TestIsAvailabilityZone(t *testing.T) { @@ -91,6 +104,8 @@ func TestGetZone(t *testing.T) { location string faultDomain string expected string + isNilResp bool + expectedErr error }{ { name: "GetZone should get real zone if only node's zone is set", @@ -117,6 +132,19 @@ func TestGetZone(t *testing.T) { zone: "1", expected: "eastus-1", }, + { + name: "GetZone should report an error if there is no `Compute` in the response", + isNilResp: true, + expectedErr: fmt.Errorf("failure of getting compute information from instance metadata"), + }, + { + name: "GetZone should report an error if the zone is invalid", + zone: "a", + location: "eastus", + faultDomain: "99", + expected: "", + expectedErr: fmt.Errorf("failed to parse zone ID \"a\": strconv.Atoi: parsing \"a\": invalid syntax"), + }, } for _, test := range testcases { @@ -125,9 +153,13 @@ func TestGetZone(t *testing.T) { t.Errorf("Test [%s] unexpected error: %v", test.name, err) } + respString := fmt.Sprintf(`{"compute":{"zone":"%s", "platformFaultDomain":"%s", "location":"%s"}}`, test.zone, test.faultDomain, test.location) + if test.isNilResp { + respString = "{}" + } mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, fmt.Sprintf(`{"compute":{"zone":"%s", "platformFaultDomain":"%s", "location":"%s"}}`, test.zone, test.faultDomain, test.location)) + fmt.Fprint(w, respString) })) go func() { http.Serve(listener, mux) @@ -141,13 +173,72 @@ func TestGetZone(t *testing.T) { zone, err := cloud.GetZone(context.Background()) if err != nil { - t.Errorf("Test [%s] unexpected error: %v", test.name, err) + if test.expectedErr == nil { + t.Errorf("Test [%s] unexpected error: %v", test.name, err) + } else { + assert.Equal(t, test.expectedErr, err) + } } if zone.FailureDomain != test.expected { t.Errorf("Test [%s] unexpected zone: %s, expected %q", test.name, zone.FailureDomain, test.expected) } - if zone.Region != cloud.Location { + if err == nil && zone.Region != cloud.Location { t.Errorf("Test [%s] unexpected region: %s, expected: %s", test.name, zone.Region, cloud.Location) } } } + +func TestMakeZone(t *testing.T) { + az := &Cloud{} + zone := az.makeZone("EASTUS", 2) + assert.Equal(t, "eastus-2", zone) +} + +func TestGetZoneByProviderID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + az := GetTestCloud(ctrl) + + zone, err := az.GetZoneByProviderID(context.Background(), "") + assert.Equal(t, errNodeNotInitialized, err) + assert.Equal(t, cloudprovider.Zone{}, zone) + + zone, err = az.GetZoneByProviderID(context.Background(), "invalid/id") + assert.NoError(t, err) + assert.Equal(t, cloudprovider.Zone{}, zone) + + mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface) + mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm-0", gomock.Any()).Return(compute.VirtualMachine{ + Zones: &[]string{"1"}, + Location: to.StringPtr("eastus"), + }, nil) + zone, err = az.GetZoneByProviderID(context.Background(), testAvailabilitySetNodeProviderID) + assert.NoError(t, err) + assert.Equal(t, cloudprovider.Zone{ + FailureDomain: "eastus-1", + Region: "eastus", + }, zone) +} + +func TestAvailabilitySetGetZoneByNodeName(t *testing.T) { + az := &Cloud{ + unmanagedNodes: sets.String{"vm-0": sets.Empty{}}, + nodeInformerSynced: func() bool { + return true + }, + } + zone, err := az.GetZoneByNodeName(context.Background(), "vm-0") + assert.NoError(t, err) + assert.Equal(t, cloudprovider.Zone{}, zone) + + az = &Cloud{ + unmanagedNodes: sets.String{"vm-0": sets.Empty{}}, + nodeInformerSynced: func() bool { + return false + }, + } + zone, err = az.GetZoneByNodeName(context.Background(), "vm-0") + assert.Equal(t, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"), err) + assert.Equal(t, cloudprovider.Zone{}, zone) +} diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient.go b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient.go index 036f0651fc5..11085241e9f 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient.go @@ -285,7 +285,7 @@ func (c *Client) SendAsync(ctx context.Context, request *http.Request) (*azure.F future, err := azure.NewFutureFromResponse(asyncResponse) if err != nil { - klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.responed", request.URL.String(), err) + klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.respond", request.URL.String(), err) return nil, asyncResponse, retry.GetError(asyncResponse, err) } @@ -337,7 +337,7 @@ func (c *Client) PutResource(ctx context.Context, resourceID string, parameters } // PutResources puts a list of resources from resources map[resourceID]parameters. -// Those resources sync requests are sequential while async requests are concurent. It 's especially +// Those resources sync requests are sequential while async requests are concurrent. It's especially // useful when the ARM API doesn't support concurrent requests. func (c *Client) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*PutResourcesResponse { if len(resources) == 0 { diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient_test.go index 1d34600801b..19645847236 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient_test.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/azure_armclient_test.go @@ -21,6 +21,7 @@ package armclient import ( "context" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "testing" @@ -31,6 +32,10 @@ import ( "k8s.io/legacy-cloud-providers/azure/retry" ) +const ( + testResourceID = "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP" +) + func TestSend(t *testing.T) { count := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -84,7 +89,7 @@ func TestSendFailure(t *testing.T) { } ctx := context.Background() - request, err := armClient.PrepareGetRequest(ctx, decorators...) + request, err := armClient.PreparePatchRequest(ctx, decorators...) assert.NoError(t, err) response, rerr := armClient.Send(ctx, request) @@ -128,7 +133,6 @@ func TestSendAsync(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count++ http.Error(w, "failed", http.StatusForbidden) - })) backoff := &retry.Backoff{Steps: 1} @@ -157,6 +161,35 @@ func TestSendAsync(t *testing.T) { assert.Equal(t, false, rerr.Retriable) } +func TestSendAsyncSuccess(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + backoff := &retry.Backoff{Steps: 1} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + pathParameters := map[string]interface{}{ + "resourceGroupName": autorest.Encode("path", "testgroup"), + "subscriptionId": autorest.Encode("path", "testid"), + "resourceName": autorest.Encode("path", "testname"), + } + decorators := []autorest.PrepareDecorator{ + autorest.WithPathParameters( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), + } + + ctx := context.Background() + request, err := armClient.PreparePostRequest(ctx, decorators...) + assert.NoError(t, err) + + future, response, rerr := armClient.SendAsync(ctx, request) + assert.Nil(t, rerr) + assert.NotNil(t, response) + assert.NotNil(t, future) +} + func TestNormalizeAzureRegion(t *testing.T) { tests := []struct { region string @@ -190,6 +223,64 @@ func TestNormalizeAzureRegion(t *testing.T) { } } +func TestGetResource(t *testing.T) { + expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?%24expand=data&api-version=2019-01-01" + + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, expectedURI, r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte("{data: testPIP}")) + count++ + })) + + backoff := &retry.Backoff{Steps: 1} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + response, rerr := armClient.GetResource(ctx, testResourceID, "data") + byteResponseBody, _ := ioutil.ReadAll(response.Body) + stringResponseBody := string(byteResponseBody) + assert.Nil(t, rerr) + assert.Equal(t, "{data: testPIP}", stringResponseBody) + assert.Equal(t, 1, count) +} + +func TestGetResourceWithDecorators(t *testing.T) { + expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01¶m1=value1¶m2=value2" + + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, expectedURI, r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte("{data: testPIP}")) + count++ + })) + + backoff := &retry.Backoff{Steps: 1} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + params := map[string]interface{}{ + "param1": "value1", + "param2": "value2", + } + decorators := []autorest.PrepareDecorator{ + autorest.WithQueryParameters(params), + } + + ctx := context.Background() + response, rerr := armClient.GetResourceWithDecorators(ctx, testResourceID, decorators) + byteResponseBody, _ := ioutil.ReadAll(response.Body) + stringResponseBody := string(byteResponseBody) + assert.Nil(t, rerr) + assert.Equal(t, "{data: testPIP}", stringResponseBody) + assert.Equal(t, 1, count) +} + func TestPutResource(t *testing.T) { expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01" operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01" @@ -225,13 +316,89 @@ func TestPutResource(t *testing.T) { armClient.client.RetryDuration = time.Millisecond * 1 ctx := context.Background() - response, rerr := armClient.PutResource(ctx, "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP", nil) + response, rerr := armClient.PutResource(ctx, testResourceID, nil) assert.Equal(t, 1, count) assert.Nil(t, response) assert.NotNil(t, rerr) assert.Equal(t, true, rerr.Retriable) } +func TestPutResources(t *testing.T) { + serverFuncs := []func(rw http.ResponseWriter, req *http.Request){ + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "PUT", req.Method) + + rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), + fmt.Sprintf("http://%s%s", req.Host, "/id/1?api-version=2019-01-01")) + rw.WriteHeader(http.StatusCreated) + }, + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "PUT", req.Method) + + rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), + fmt.Sprintf("http://%s%s", req.Host, "/id/2?api-version=2019-01-01")) + rw.WriteHeader(http.StatusInternalServerError) + }, + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "GET", req.Method) + + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) + }, + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "GET", req.Method) + + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) + }, + } + + i, total := 0, 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + serverFuncs[i](w, r) + i++ + if i > 3 { + i = 3 + } + total++ + })) + + backoff := &retry.Backoff{Steps: 1} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + resources := map[string]interface{}{ + "/id/1": nil, + "/id/2": nil, + } + responses := armClient.PutResources(ctx, nil) + assert.Nil(t, responses) + responses = armClient.PutResources(ctx, resources) + assert.NotNil(t, responses) + assert.Equal(t, 3, total) +} + +func TestPutResourceAsync(t *testing.T) { + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + http.Error(w, "failed", http.StatusInternalServerError) + })) + + backoff := &retry.Backoff{Steps: 3} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + resourceID := testResourceID + future, rerr := armClient.PutResourceAsync(ctx, resourceID, "") + assert.Equal(t, 3, count) + assert.Nil(t, future) + assert.NotNil(t, rerr) + assert.Equal(t, true, rerr.Retriable) +} + func TestDeleteResourceAsync(t *testing.T) { count := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -244,10 +411,146 @@ func TestDeleteResourceAsync(t *testing.T) { armClient.client.RetryDuration = time.Millisecond * 1 ctx := context.Background() - resourceID := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP" + resourceID := testResourceID future, rerr := armClient.DeleteResourceAsync(ctx, resourceID, "") assert.Equal(t, 3, count) assert.Nil(t, future) assert.NotNil(t, rerr) assert.Equal(t, true, rerr.Retriable) } + +func TestPatchResource(t *testing.T) { + expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01" + operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01" + handlers := []func(http.ResponseWriter, *http.Request){ + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, expectedURI, req.URL.String()) + rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), + fmt.Sprintf("http://%s%s", req.Host, operationURI)) + rw.WriteHeader(http.StatusCreated) + }, + + func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "GET", req.Method) + assert.Equal(t, operationURI, req.URL.String()) + + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) + }, + } + + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handlers[count](w, r) + count++ + if count > 1 { + count = 1 + } + })) + + backoff := &retry.Backoff{Steps: 1} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + response, rerr := armClient.PatchResource(ctx, testResourceID, nil) + assert.Equal(t, 1, count) + assert.Nil(t, response) + assert.NotNil(t, rerr) + assert.Equal(t, true, rerr.Retriable) +} + +func TestPostResource(t *testing.T) { + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + http.Error(w, "failed", http.StatusInternalServerError) + })) + + backoff := &retry.Backoff{Steps: 3} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + resourceID := testResourceID + future, rerr := armClient.PostResource(ctx, resourceID, "post", "") + assert.Equal(t, 3, count) + assert.NotNil(t, future) + assert.NotNil(t, rerr) + assert.Equal(t, true, rerr.Retriable) +} + +func TestDeleteResource(t *testing.T) { + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + http.Error(w, "failed", http.StatusInternalServerError) + })) + + backoff := &retry.Backoff{Steps: 3} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + resourceID := testResourceID + rerr := armClient.DeleteResource(ctx, resourceID, "") + assert.Equal(t, 3, count) + assert.NotNil(t, rerr) + assert.Equal(t, true, rerr.Retriable) +} + +func TestHeadResource(t *testing.T) { + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + http.Error(w, "failed", http.StatusInternalServerError) + })) + + backoff := &retry.Backoff{Steps: 3} + armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) + armClient.client.RetryDuration = time.Millisecond * 1 + + ctx := context.Background() + resourceID := testResourceID + response, rerr := armClient.HeadResource(ctx, resourceID) + assert.Equal(t, 3, count) + assert.NotNil(t, response) + assert.NotNil(t, rerr) + assert.Equal(t, true, rerr.Retriable) +} + +func TestGetResourceID(t *testing.T) { + expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name" + + resourceID := GetResourceID("sub", "rg", "type", "name") + assert.Equal(t, expectedResourceID, resourceID) +} + +func TestGetChildResourceID(t *testing.T) { + expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2/name-3" + + resourceID := GetChildResourceID("sub", "rg", "type", "name-1", "name-2", "name-3") + assert.Equal(t, expectedResourceID, resourceID) +} + +func TestGetChildResourcesListID(t *testing.T) { + expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2" + + resourceID := GetChildResourcesListID("sub", "rg", "type", "name-1", "name-2") + assert.Equal(t, expectedResourceID, resourceID) +} + +func TestGetProviderResourceID(t *testing.T) { + expectedResourceID := "/subscriptions/sub/providers/namespace" + + resourceID := GetProviderResourceID("sub", "namespace") + assert.Equal(t, expectedResourceID, resourceID) +} + +func TestGetProviderResourcesListID(t *testing.T) { + expectedResourceID := "/subscriptions/sub/providers" + + resourceID := GetProviderResourcesListID("sub") + assert.Equal(t, expectedResourceID, resourceID) +} diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/BUILD b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/BUILD index 8a8057fbfdd..a34f5bef375 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/BUILD +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/BUILD @@ -31,6 +31,7 @@ go_test( "//staging/src/k8s.io/legacy-cloud-providers/azure/clients:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient:go_default_library", + "//staging/src/k8s.io/legacy-cloud-providers/azure/retry:go_default_library", "//vendor/github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest/to:go_default_library", diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/azure_diskclient_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/azure_diskclient_test.go index 12285a1fc25..2d9322dc859 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/azure_diskclient_test.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/diskclient/azure_diskclient_test.go @@ -25,6 +25,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" "github.com/Azure/go-autorest/autorest" @@ -35,8 +36,30 @@ import ( azclients "k8s.io/legacy-cloud-providers/azure/clients" "k8s.io/legacy-cloud-providers/azure/clients/armclient" "k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" + "k8s.io/legacy-cloud-providers/azure/retry" ) +func TestNew(t *testing.T) { + config := &azclients.ClientConfig{ + SubscriptionID: "sub", + ResourceManagerEndpoint: "endpoint", + Location: "eastus", + RateLimitConfig: &azclients.RateLimitConfig{ + CloudProviderRateLimit: true, + CloudProviderRateLimitQPS: 0.5, + CloudProviderRateLimitBucket: 1, + CloudProviderRateLimitQPSWrite: 0.5, + CloudProviderRateLimitBucketWrite: 1, + }, + Backoff: &retry.Backoff{Steps: 1}, + } + + diskClient := New(config) + assert.Equal(t, "sub", diskClient.subscriptionID) + assert.NotEmpty(t, diskClient.rateLimiterReader) + assert.NotEmpty(t, diskClient.rateLimiterWriter) +} + func TestGetNotFound(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -79,6 +102,31 @@ func TestGetInternalError(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) } +func TestGetThrottle(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1" + response := &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient := mockarmclient.NewMockInterface(ctrl) + armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + + diskClient := getTestDiskClient(armClient) + result, rerr := diskClient.Get(context.TODO(), "rg", "disk1") + assert.Empty(t, result) + assert.Equal(t, throttleErr, rerr) +} + func TestCreateOrUpdate(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -95,6 +143,22 @@ func TestCreateOrUpdate(t *testing.T) { diskClient := getTestDiskClient(armClient) rerr := diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk) assert.Nil(t, rerr) + + response = &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + + armClient.EXPECT().PutResource(gomock.Any(), to.String(disk.ID), disk).Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + rerr = diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk) + assert.Equal(t, throttleErr, rerr) } func TestDelete(t *testing.T) { @@ -108,6 +172,16 @@ func TestDelete(t *testing.T) { diskClient := getTestDiskClient(armClient) rerr := diskClient.Delete(context.TODO(), "rg", "disk1") assert.Nil(t, rerr) + + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient.EXPECT().DeleteResource(gomock.Any(), to.String(r.ID), "").Return(throttleErr).Times(1) + rerr = diskClient.Delete(context.TODO(), "rg", "disk1") + assert.Equal(t, throttleErr, rerr) } func getTestDisk(name string) compute.Disk { diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/BUILD b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/BUILD index d392c806bac..f4837d868c8 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/BUILD +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/BUILD @@ -31,6 +31,7 @@ go_test( "//staging/src/k8s.io/legacy-cloud-providers/azure/clients:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient:go_default_library", + "//staging/src/k8s.io/legacy-cloud-providers/azure/retry:go_default_library", "//vendor/github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest/to:go_default_library", diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/azure_interfaceclient_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/azure_interfaceclient_test.go index 269ef1b9f1e..32cb810c818 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/azure_interfaceclient_test.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/azure_interfaceclient_test.go @@ -25,6 +25,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" "github.com/Azure/go-autorest/autorest" @@ -35,8 +36,30 @@ import ( azclients "k8s.io/legacy-cloud-providers/azure/clients" "k8s.io/legacy-cloud-providers/azure/clients/armclient" "k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" + "k8s.io/legacy-cloud-providers/azure/retry" ) +func TestNew(t *testing.T) { + config := &azclients.ClientConfig{ + SubscriptionID: "sub", + ResourceManagerEndpoint: "endpoint", + Location: "eastus", + RateLimitConfig: &azclients.RateLimitConfig{ + CloudProviderRateLimit: true, + CloudProviderRateLimitQPS: 0.5, + CloudProviderRateLimitBucket: 1, + CloudProviderRateLimitQPSWrite: 0.5, + CloudProviderRateLimitBucketWrite: 1, + }, + Backoff: &retry.Backoff{Steps: 1}, + } + + interfaceClient := New(config) + assert.Equal(t, "sub", interfaceClient.subscriptionID) + assert.NotEmpty(t, interfaceClient.rateLimiterReader) + assert.NotEmpty(t, interfaceClient.rateLimiterWriter) +} + func TestGetNotFound(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -79,6 +102,31 @@ func TestGetInternalError(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) } +func TestGetThrottle(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic1" + response := &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient := mockarmclient.NewMockInterface(ctrl) + armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + + nicClient := getTestInterfaceClient(armClient) + result, rerr := nicClient.Get(context.TODO(), "rg", "nic1", "") + assert.Empty(t, result) + assert.Equal(t, throttleErr, rerr) +} + func TestGetVirtualMachineScaleSetNetworkInterface(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -101,6 +149,23 @@ func TestGetVirtualMachineScaleSetNetworkInterface(t *testing.T) { result, rerr := nicClient.GetVirtualMachineScaleSetNetworkInterface(context.TODO(), "rg", "vmss", "0", "nic1", "") assert.Equal(t, expected, result) assert.Nil(t, rerr) + + response = &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + + armClient.EXPECT().GetResourceWithDecorators(gomock.Any(), resourceID, gomock.Any()).Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + result, rerr = nicClient.GetVirtualMachineScaleSetNetworkInterface(context.TODO(), "rg", "vmss", "0", "nic1", "test") + assert.Empty(t, result) + assert.Equal(t, throttleErr, rerr) } func TestCreateOrUpdate(t *testing.T) { @@ -119,6 +184,22 @@ func TestCreateOrUpdate(t *testing.T) { nicClient := getTestInterfaceClient(armClient) rerr := nicClient.CreateOrUpdate(context.TODO(), "rg", "nic1", testInterface) assert.Nil(t, rerr) + + response = &http.Response{ + StatusCode: http.StatusNoContent, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + noContentErr := &retry.Error{ + HTTPStatusCode: http.StatusNoContent, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + + armClient.EXPECT().PutResource(gomock.Any(), to.String(testInterface.ID), testInterface).Return(response, noContentErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + rerr = nicClient.CreateOrUpdate(context.TODO(), "rg", "nic1", testInterface) + assert.Equal(t, noContentErr, rerr) } func TestDelete(t *testing.T) { @@ -129,9 +210,20 @@ func TestDelete(t *testing.T) { armClient := mockarmclient.NewMockInterface(ctrl) armClient.EXPECT().DeleteResource(gomock.Any(), to.String(r.ID), "").Return(nil).Times(1) - diskClient := getTestInterfaceClient(armClient) - rerr := diskClient.Delete(context.TODO(), "rg", "interface1") + nicClient := getTestInterfaceClient(armClient) + rerr := nicClient.Delete(context.TODO(), "rg", "interface1") assert.Nil(t, rerr) + + noContentErr := &retry.Error{ + HTTPStatusCode: http.StatusNoContent, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient.EXPECT().DeleteResource(gomock.Any(), to.String(r.ID), "").Return(noContentErr).Times(1) + + rerr = nicClient.Delete(context.TODO(), "rg", "interface1") + assert.Equal(t, noContentErr, rerr) } func getTestInterface(name string) network.Interface { diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/BUILD b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/BUILD index 0359de1f74c..08a1d88d4ee 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/BUILD +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/BUILD @@ -32,6 +32,7 @@ go_test( "//staging/src/k8s.io/legacy-cloud-providers/azure/clients:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient:go_default_library", "//staging/src/k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient:go_default_library", + "//staging/src/k8s.io/legacy-cloud-providers/azure/retry:go_default_library", "//vendor/github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest/to:go_default_library", diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/azure_loadbalancerclient_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/azure_loadbalancerclient_test.go index 54ece9bcc2f..eb566d0b611 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/azure_loadbalancerclient_test.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/azure_loadbalancerclient_test.go @@ -26,6 +26,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" "github.com/Azure/go-autorest/autorest" @@ -36,8 +37,30 @@ import ( azclients "k8s.io/legacy-cloud-providers/azure/clients" "k8s.io/legacy-cloud-providers/azure/clients/armclient" "k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" + "k8s.io/legacy-cloud-providers/azure/retry" ) +func TestNew(t *testing.T) { + config := &azclients.ClientConfig{ + SubscriptionID: "sub", + ResourceManagerEndpoint: "endpoint", + Location: "eastus", + RateLimitConfig: &azclients.RateLimitConfig{ + CloudProviderRateLimit: true, + CloudProviderRateLimitQPS: 0.5, + CloudProviderRateLimitBucket: 1, + CloudProviderRateLimitQPSWrite: 0.5, + CloudProviderRateLimitBucketWrite: 1, + }, + Backoff: &retry.Backoff{Steps: 1}, + } + + lbClient := New(config) + assert.Equal(t, "sub", lbClient.subscriptionID) + assert.NotEmpty(t, lbClient.rateLimiterReader) + assert.NotEmpty(t, lbClient.rateLimiterWriter) +} + func TestGetNotFound(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -80,6 +103,31 @@ func TestGetInternalError(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) } +func TestGetThrottle(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1" + response := &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient := mockarmclient.NewMockInterface(ctrl) + armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + + lbClient := getTestLoadBalancerClient(armClient) + result, rerr := lbClient.Get(context.TODO(), "rg", "lb1", "") + assert.Empty(t, result) + assert.Equal(t, throttleErr, rerr) +} + func TestList(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -100,6 +148,74 @@ func TestList(t *testing.T) { result, rerr := lbClient.List(context.TODO(), "rg") assert.Nil(t, rerr) assert.Equal(t, 3, len(result)) + + response := &http.Response{ + StatusCode: http.StatusTooManyRequests, + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), + } + throttleErr := &retry.Error{ + HTTPStatusCode: http.StatusTooManyRequests, + RawError: fmt.Errorf("error"), + Retriable: true, + RetryAfter: time.Unix(100, 0), + } + armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) + _, rerr = lbClient.List(context.TODO(), "rg") + assert.Equal(t, throttleErr, rerr) +} + +func TestListNextResultsMultiPages(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + prepareErr error + sendErr *retry.Error + }{ + { + prepareErr: nil, + sendErr: nil, + }, + { + prepareErr: fmt.Errorf("error"), + }, + { + sendErr: &retry.Error{RawError: fmt.Errorf("error")}, + }, + } + + lastResult := network.LoadBalancerListResult{ + NextLink: to.StringPtr("next"), + } + + for _, test := range tests { + armClient := mockarmclient.NewMockInterface(ctrl) + req := &http.Request{ + Method: "GET", + } + armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr) + if test.prepareErr == nil { + armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))), + }, test.sendErr) + armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()) + } + + lbClient := getTestLoadBalancerClient(armClient) + result, err := lbClient.listNextResults(context.TODO(), lastResult) + if test.prepareErr != nil || test.sendErr != nil { + assert.NotNil(t, err) + } else { + assert.NoError(t, err) + } + if test.prepareErr != nil { + assert.Empty(t, result) + } else { + assert.NotEmpty(t, result) + } + } } func TestCreateOrUpdate(t *testing.T) { @@ -120,6 +236,37 @@ func TestCreateOrUpdate(t *testing.T) { assert.Nil(t, rerr) } +func TestDelete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + description string + armClientErr *retry.Error + expectedErr *retry.Error + }{ + { + description: "Delete should report the throttling error", + armClientErr: &retry.Error{HTTPStatusCode: http.StatusTooManyRequests}, + expectedErr: &retry.Error{HTTPStatusCode: http.StatusTooManyRequests}, + }, + { + description: "Delete should not report any error if there's no error from arm client", + }, + } + + lb := getTestLoadBalancer("lb1") + + for _, test := range tests { + armClient := mockarmclient.NewMockInterface(ctrl) + armClient.EXPECT().DeleteResource(gomock.Any(), to.String(lb.ID), "").Return(test.armClientErr) + + lbClient := getTestLoadBalancerClient(armClient) + rerr := lbClient.Delete(context.TODO(), "rg", "lb1") + assert.Equal(t, test.expectedErr, rerr) + } +} + func getTestLoadBalancer(name string) network.LoadBalancer { return network.LoadBalancer{ ID: to.StringPtr(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/%s", name)),