github.com/m3db/m3@v1.5.0/src/cluster/placementhandler/set_test.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package placementhandler
    22  
    23  import (
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/m3db/m3/src/cluster/generated/proto/placementpb"
    30  	"github.com/m3db/m3/src/cluster/kv"
    31  	"github.com/m3db/m3/src/cluster/placement"
    32  	"github.com/m3db/m3/src/cluster/placementhandler/handleroptions"
    33  	"github.com/m3db/m3/src/query/generated/proto/admin"
    34  	"github.com/m3db/m3/src/x/instrument"
    35  	xtest "github.com/m3db/m3/src/x/test"
    36  
    37  	"github.com/gogo/protobuf/jsonpb"
    38  	"github.com/golang/mock/gomock"
    39  	"github.com/stretchr/testify/assert"
    40  	"github.com/stretchr/testify/require"
    41  )
    42  
    43  var (
    44  	setExistingTestPlacementProto = &placementpb.Placement{
    45  		Instances: map[string]*placementpb.Instance{
    46  			"host1": {
    47  				Id:             "host1",
    48  				IsolationGroup: "rack1",
    49  				Zone:           "test",
    50  				Weight:         1,
    51  				Endpoint:       "http://host1:1234",
    52  				Hostname:       "host1",
    53  				Port:           1234,
    54  			},
    55  		},
    56  	}
    57  	setNewTestPlacementProto = &placementpb.Placement{
    58  		Instances: map[string]*placementpb.Instance{
    59  			"host1": {
    60  				Id:             "host1",
    61  				IsolationGroup: "rack1",
    62  				Zone:           "test",
    63  				Weight:         1,
    64  				Endpoint:       "http://host1:1234",
    65  				Hostname:       "host1",
    66  				Port:           1234,
    67  			},
    68  			"host2": {
    69  				Id:             "host2",
    70  				IsolationGroup: "rack1",
    71  				Zone:           "test",
    72  				Weight:         1,
    73  				Endpoint:       "http://host2:1234",
    74  				Hostname:       "host2",
    75  				Port:           1234,
    76  			},
    77  		},
    78  	}
    79  	setTestPlacementReqProto = &admin.PlacementSetRequest{
    80  		Placement: setNewTestPlacementProto,
    81  		Version:   0,
    82  		Confirm:   true,
    83  	}
    84  )
    85  
    86  func TestPlacementSetHandler(t *testing.T) {
    87  	runForAllAllowedServices(func(serviceName string) {
    88  		var url string
    89  		switch serviceName {
    90  		case handleroptions.M3DBServiceName:
    91  			url = M3DBSetURL
    92  		case handleroptions.M3AggregatorServiceName:
    93  			url = M3AggSetURL
    94  		case handleroptions.M3CoordinatorServiceName:
    95  			url = M3CoordinatorSetURL
    96  		default:
    97  			require.FailNow(t, "unexpected service name")
    98  		}
    99  
   100  		ctrl := gomock.NewController(t)
   101  		defer ctrl.Finish()
   102  
   103  		mockClient, mockPlacementService := SetupPlacementTest(t, ctrl)
   104  		handlerOpts, err := NewHandlerOptions(
   105  			mockClient, placement.Configuration{}, nil, instrument.NewOptions())
   106  		require.NoError(t, err)
   107  		handler := NewSetHandler(handlerOpts)
   108  
   109  		// Test placement init success
   110  		reqBody, err := (&jsonpb.Marshaler{}).MarshalToString(setTestPlacementReqProto)
   111  		require.NoError(t, err)
   112  
   113  		req := httptest.NewRequest(SetHTTPMethod, url, strings.NewReader(reqBody))
   114  		require.NotNil(t, req)
   115  
   116  		existingPlacement, err := placement.NewPlacementFromProto(setExistingTestPlacementProto)
   117  		require.NoError(t, err)
   118  
   119  		mockPlacementService.EXPECT().
   120  			Placement().
   121  			Return(existingPlacement, nil)
   122  
   123  		newPlacement, err := placement.NewPlacementFromProto(setNewTestPlacementProto)
   124  		require.NoError(t, err)
   125  
   126  		mockPlacementService.EXPECT().
   127  			CheckAndSet(gomock.Any(), gomock.Any()).
   128  			Return(newPlacement, nil)
   129  
   130  		svcDefaults := handleroptions.ServiceNameAndDefaults{
   131  			ServiceName: serviceName,
   132  		}
   133  
   134  		w := httptest.NewRecorder()
   135  		handler.ServeHTTP(svcDefaults, w, req)
   136  		resp := w.Result()
   137  		body := w.Body.String()
   138  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   139  
   140  		expectedBody, err := (&jsonpb.Marshaler{
   141  			EmitDefaults: true,
   142  		}).MarshalToString(&admin.PlacementSetResponse{
   143  			Placement: setNewTestPlacementProto,
   144  			DryRun:    !setTestPlacementReqProto.Confirm,
   145  		})
   146  		require.NoError(t, err)
   147  
   148  		expected := xtest.MustPrettyJSONString(t, expectedBody)
   149  		actual := xtest.MustPrettyJSONString(t, body)
   150  
   151  		assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   152  	})
   153  }
   154  
   155  func TestPlacementSetHandler_NewPlacement(t *testing.T) {
   156  	runForAllAllowedServices(func(serviceName string) {
   157  		var url string
   158  		switch serviceName {
   159  		case handleroptions.M3DBServiceName:
   160  			url = M3DBSetURL
   161  		case handleroptions.M3AggregatorServiceName:
   162  			url = M3AggSetURL
   163  		case handleroptions.M3CoordinatorServiceName:
   164  			url = M3CoordinatorSetURL
   165  		default:
   166  			require.FailNow(t, "unexpected service name")
   167  		}
   168  
   169  		ctrl := gomock.NewController(t)
   170  		defer ctrl.Finish()
   171  
   172  		mockClient, mockPlacementService := SetupPlacementTest(t, ctrl)
   173  		handlerOpts, err := NewHandlerOptions(
   174  			mockClient, placement.Configuration{}, nil, instrument.NewOptions())
   175  		require.NoError(t, err)
   176  		handler := NewSetHandler(handlerOpts)
   177  
   178  		// Test placement init success
   179  		reqBody, err := (&jsonpb.Marshaler{}).MarshalToString(setTestPlacementReqProto)
   180  		require.NoError(t, err)
   181  
   182  		req := httptest.NewRequest(SetHTTPMethod, url, strings.NewReader(reqBody))
   183  		require.NotNil(t, req)
   184  
   185  		mockPlacementService.EXPECT().
   186  			Placement().
   187  			Return(nil, kv.ErrNotFound)
   188  
   189  		newPlacement, err := placement.NewPlacementFromProto(setNewTestPlacementProto)
   190  		require.NoError(t, err)
   191  
   192  		mockPlacementService.EXPECT().
   193  			SetIfNotExist(gomock.Any()).
   194  			Return(newPlacement, nil)
   195  
   196  		svcDefaults := handleroptions.ServiceNameAndDefaults{
   197  			ServiceName: serviceName,
   198  		}
   199  
   200  		w := httptest.NewRecorder()
   201  		handler.ServeHTTP(svcDefaults, w, req)
   202  		resp := w.Result()
   203  		body := w.Body.String()
   204  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   205  
   206  		expectedBody, err := (&jsonpb.Marshaler{
   207  			EmitDefaults: true,
   208  		}).MarshalToString(&admin.PlacementSetResponse{
   209  			Placement: setNewTestPlacementProto,
   210  			DryRun:    !setTestPlacementReqProto.Confirm,
   211  		})
   212  		require.NoError(t, err)
   213  
   214  		expected := xtest.MustPrettyJSONString(t, expectedBody)
   215  		actual := xtest.MustPrettyJSONString(t, body)
   216  
   217  		assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   218  		assert.Equal(t, 0, newPlacement.Version())
   219  	})
   220  }
   221  
   222  func TestPlacementSetHandler_ValidatePlacementWithoutForce(t *testing.T) {
   223  	ctrl := gomock.NewController(t)
   224  	defer ctrl.Finish()
   225  
   226  	mockClient, mockPlacementService := SetupPlacementTest(t, ctrl)
   227  	handlerOpts, err := NewHandlerOptions(
   228  		mockClient, placement.Configuration{}, nil, instrument.NewOptions())
   229  	require.NoError(t, err)
   230  	handler := NewSetHandler(handlerOpts)
   231  
   232  	badReqProto := &admin.PlacementSetRequest{
   233  		Placement: &placementpb.Placement{
   234  			Instances: map[string]*placementpb.Instance{
   235  				"host1": {
   236  					Id:             "host1",
   237  					IsolationGroup: "rack1",
   238  					Zone:           "test",
   239  					Weight:         1,
   240  					Endpoint:       "http://host1:1234",
   241  					Hostname:       "host1",
   242  					Port:           1234,
   243  					Shards: []*placementpb.Shard{
   244  						&placementpb.Shard{
   245  							Id:    0,
   246  							State: placementpb.ShardState_AVAILABLE,
   247  						},
   248  						&placementpb.Shard{
   249  							Id:    1,
   250  							State: placementpb.ShardState_AVAILABLE,
   251  						},
   252  					},
   253  				},
   254  				"host2": {
   255  					Id:             "host2",
   256  					IsolationGroup: "rack1",
   257  					Zone:           "test",
   258  					Weight:         1,
   259  					Endpoint:       "http://host2:1234",
   260  					Hostname:       "host2",
   261  					Port:           1234,
   262  					Shards: []*placementpb.Shard{
   263  						&placementpb.Shard{
   264  							Id:       0,
   265  							State:    placementpb.ShardState_INITIALIZING,
   266  							SourceId: "host1",
   267  						},
   268  						&placementpb.Shard{
   269  							Id:       1,
   270  							State:    placementpb.ShardState_INITIALIZING,
   271  							SourceId: "host1",
   272  						},
   273  					},
   274  				},
   275  			},
   276  			IsSharded:     true,
   277  			NumShards:     2,
   278  			ReplicaFactor: 2,
   279  		},
   280  		Version: 0,
   281  		Confirm: true,
   282  	}
   283  
   284  	reqBody, err := (&jsonpb.Marshaler{}).MarshalToString(badReqProto)
   285  	require.NoError(t, err)
   286  
   287  	req := httptest.NewRequest(SetHTTPMethod, M3DBSetURL, strings.NewReader(reqBody))
   288  	require.NotNil(t, req)
   289  
   290  	existingPlacementProto := &placementpb.Placement{
   291  		Instances: map[string]*placementpb.Instance{
   292  			"host1": {
   293  				Id:             "host1",
   294  				IsolationGroup: "rack1",
   295  				Zone:           "test",
   296  				Weight:         1,
   297  				Endpoint:       "http://host1:1234",
   298  				Hostname:       "host1",
   299  				Port:           1234,
   300  				Shards: []*placementpb.Shard{
   301  					&placementpb.Shard{
   302  						Id:    0,
   303  						State: placementpb.ShardState_AVAILABLE,
   304  					},
   305  					&placementpb.Shard{
   306  						Id:    1,
   307  						State: placementpb.ShardState_AVAILABLE,
   308  					},
   309  				},
   310  			},
   311  		},
   312  		IsSharded:     true,
   313  		NumShards:     2,
   314  		ReplicaFactor: 1,
   315  	}
   316  
   317  	existingPlacement, err := placement.NewPlacementFromProto(existingPlacementProto)
   318  	require.NoError(t, err)
   319  
   320  	mockPlacementService.EXPECT().
   321  		Placement().
   322  		Return(existingPlacement, nil)
   323  
   324  	svcDefaults := handleroptions.ServiceNameAndDefaults{
   325  		ServiceName: handleroptions.M3DBServiceName,
   326  	}
   327  
   328  	w := httptest.NewRecorder()
   329  	handler.ServeHTTP(svcDefaults, w, req)
   330  	resp := w.Result()
   331  	body := w.Body.String()
   332  	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   333  	assert.True(t, strings.Contains(body, "unable to validate new placement"))
   334  	assert.True(t, strings.Contains(body, "instance host2 has initializing shard 0 with source ID host1 but leaving instance has shard with state Available"))
   335  }