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 }