github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/namespace/update_test.go (about) 1 // Copyright (c) 2020 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 namespace 22 23 import ( 24 "errors" 25 "fmt" 26 "io/ioutil" 27 "net/http" 28 "net/http/httptest" 29 "strings" 30 "testing" 31 32 "github.com/m3db/m3/src/cluster/kv" 33 nsproto "github.com/m3db/m3/src/dbnode/generated/proto/namespace" 34 "github.com/m3db/m3/src/query/generated/proto/admin" 35 "github.com/m3db/m3/src/x/instrument" 36 xjson "github.com/m3db/m3/src/x/json" 37 xtest "github.com/m3db/m3/src/x/test" 38 39 "github.com/gogo/protobuf/types" 40 "github.com/golang/mock/gomock" 41 "github.com/stretchr/testify/assert" 42 "github.com/stretchr/testify/require" 43 ) 44 45 const ( 46 testUpdateJSON = ` 47 { 48 "name": "testNamespace", 49 "options": { 50 "retentionOptions": { 51 "retentionPeriodDuration": "96h" 52 }, 53 "runtimeOptions": { 54 "writeIndexingPerCPUConcurrency": 16 55 }, 56 "aggregationOptions": { 57 "aggregations": [ 58 { 59 "aggregated": true, 60 "attributes": { 61 "resolutionDuration": "5m" 62 } 63 } 64 ] 65 }, 66 "extendedOptions": { 67 "type": "testExtendedOptions", 68 "options": { 69 "value": "bar" 70 } 71 } 72 } 73 } 74 ` 75 76 testUpdateJSONNop = ` 77 { 78 "name": "testNamespace", 79 "options": { 80 "retentionOptions": {} 81 } 82 } 83 ` 84 ) 85 86 func TestNamespaceUpdateHandler(t *testing.T) { 87 ctrl := gomock.NewController(t) 88 defer ctrl.Finish() 89 90 mockClient, mockKV := setupNamespaceTest(t, ctrl) 91 updateHandler := NewUpdateHandler(mockClient, instrument.NewOptions()) 92 mockClient.EXPECT().Store(gomock.Any()).Return(mockKV, nil).Times(2) 93 94 // Error case where required fields are not set 95 w := httptest.NewRecorder() 96 97 jsonInput := xjson.Map{ 98 "name": "testNamespace", 99 "options": xjson.Map{}, 100 } 101 102 req := httptest.NewRequest("POST", "/namespace", 103 xjson.MustNewTestReader(t, jsonInput)) 104 require.NotNil(t, req) 105 106 updateHandler.ServeHTTP(svcDefaults, w, req) 107 108 resp := w.Result() 109 body, err := ioutil.ReadAll(resp.Body) 110 assert.NoError(t, err) 111 assert.Equal(t, http.StatusBadRequest, resp.StatusCode) 112 assert.JSONEq(t, 113 `{"status":"error","error":"unable to validate update request: update options cannot be empty"}`, 114 string(body)) 115 116 // Test good case. Note: there is no way to tell the difference between a boolean 117 // being false and it not being set by a user. 118 w = httptest.NewRecorder() 119 120 req = httptest.NewRequest("PUT", "/namespace", strings.NewReader(testUpdateJSON)) 121 require.NotNil(t, req) 122 123 extendedOpts := xtest.NewTestExtendedOptionsProto("foo") 124 125 registry := nsproto.Registry{ 126 Namespaces: map[string]*nsproto.NamespaceOptions{ 127 "testNamespace": { 128 BootstrapEnabled: true, 129 CacheBlocksOnRetrieve: &types.BoolValue{Value: true}, 130 FlushEnabled: true, 131 SnapshotEnabled: true, 132 WritesToCommitLog: true, 133 CleanupEnabled: false, 134 RepairEnabled: false, 135 RetentionOptions: &nsproto.RetentionOptions{ 136 RetentionPeriodNanos: 172800000000000, 137 BlockSizeNanos: 7200000000000, 138 BufferFutureNanos: 600000000000, 139 BufferPastNanos: 600000000000, 140 BlockDataExpiry: true, 141 BlockDataExpiryAfterNotAccessPeriodNanos: 3600000000000, 142 }, 143 ExtendedOptions: extendedOpts, 144 }, 145 }, 146 } 147 148 mockValue := kv.NewMockValue(ctrl) 149 mockValue.EXPECT().Unmarshal(gomock.Any()).Return(nil).SetArg(0, registry) 150 mockValue.EXPECT().Version().Return(0) 151 mockKV.EXPECT().Get(M3DBNodeNamespacesKey).Return(mockValue, nil) 152 153 mockKV.EXPECT().CheckAndSet(M3DBNodeNamespacesKey, gomock.Any(), gomock.Not(nil)).Return(1, nil) 154 updateHandler.ServeHTTP(svcDefaults, w, req) 155 156 resp = w.Result() 157 body, _ = ioutil.ReadAll(resp.Body) 158 assert.Equal(t, http.StatusOK, resp.StatusCode) 159 160 expected := xtest.MustPrettyJSONMap(t, 161 xjson.Map{ 162 "registry": xjson.Map{ 163 "namespaces": xjson.Map{ 164 "testNamespace": xjson.Map{ 165 "aggregationOptions": xjson.Map{ 166 "aggregations": xjson.Array{ 167 xjson.Map{ 168 "aggregated": true, 169 "attributes": xjson.Map{ 170 "resolutionNanos": "300000000000", 171 "downsampleOptions": xjson.Map{ 172 "all": true, 173 }, 174 }, 175 }, 176 }, 177 }, 178 "bootstrapEnabled": true, 179 "cacheBlocksOnRetrieve": true, 180 "flushEnabled": true, 181 "writesToCommitLog": true, 182 "cleanupEnabled": false, 183 "repairEnabled": false, 184 "retentionOptions": xjson.Map{ 185 "retentionPeriodNanos": "345600000000000", 186 "blockSizeNanos": "7200000000000", 187 "bufferFutureNanos": "600000000000", 188 "bufferPastNanos": "600000000000", 189 "blockDataExpiry": true, 190 "blockDataExpiryAfterNotAccessPeriodNanos": "3600000000000", 191 "futureRetentionPeriodNanos": "0", 192 }, 193 "snapshotEnabled": true, 194 "indexOptions": xjson.Map{ 195 "enabled": false, 196 "blockSizeNanos": "7200000000000", 197 }, 198 "runtimeOptions": xjson.Map{ 199 "flushIndexingPerCPUConcurrency": nil, 200 "writeIndexingPerCPUConcurrency": 16, 201 }, 202 "schemaOptions": nil, 203 "stagingState": xjson.Map{"status": "UNKNOWN"}, 204 "coldWritesEnabled": false, 205 "extendedOptions": xtest.NewTestExtendedOptionsJSON("bar"), 206 }, 207 }, 208 }, 209 }) 210 211 actual := xtest.MustPrettyJSONString(t, string(body)) 212 213 assert.Equal(t, expected, actual, 214 xtest.Diff(expected, actual)) 215 216 // Ensure an empty request respects existing namespaces. 217 w = httptest.NewRecorder() 218 req = httptest.NewRequest("PUT", "/namespace", strings.NewReader(testUpdateJSONNop)) 219 require.NotNil(t, req) 220 221 mockValue = kv.NewMockValue(ctrl) 222 mockValue.EXPECT().Unmarshal(gomock.Any()).Return(nil).SetArg(0, registry) 223 mockValue.EXPECT().Version().Return(0) 224 mockKV.EXPECT().Get(M3DBNodeNamespacesKey).Return(mockValue, nil) 225 226 mockKV.EXPECT().CheckAndSet(M3DBNodeNamespacesKey, gomock.Any(), gomock.Not(nil)).Return(1, nil) 227 updateHandler.ServeHTTP(svcDefaults, w, req) 228 229 resp = w.Result() 230 body, _ = ioutil.ReadAll(resp.Body) 231 assert.Equal(t, http.StatusOK, resp.StatusCode) 232 233 expected = xtest.MustPrettyJSONMap(t, 234 xjson.Map{ 235 "registry": xjson.Map{ 236 "namespaces": xjson.Map{ 237 "testNamespace": xjson.Map{ 238 "aggregationOptions": nil, 239 "bootstrapEnabled": true, 240 "cacheBlocksOnRetrieve": true, 241 "flushEnabled": true, 242 "writesToCommitLog": true, 243 "cleanupEnabled": false, 244 "repairEnabled": false, 245 "retentionOptions": xjson.Map{ 246 "retentionPeriodNanos": "172800000000000", 247 "blockSizeNanos": "7200000000000", 248 "bufferFutureNanos": "600000000000", 249 "bufferPastNanos": "600000000000", 250 "blockDataExpiry": true, 251 "blockDataExpiryAfterNotAccessPeriodNanos": "3600000000000", 252 "futureRetentionPeriodNanos": "0", 253 }, 254 "snapshotEnabled": true, 255 "indexOptions": xjson.Map{ 256 "enabled": false, 257 "blockSizeNanos": "7200000000000", 258 }, 259 "runtimeOptions": nil, 260 "schemaOptions": nil, 261 "stagingState": xjson.Map{"status": "UNKNOWN"}, 262 "coldWritesEnabled": false, 263 "extendedOptions": xtest.NewTestExtendedOptionsJSON("foo"), 264 }, 265 }, 266 }, 267 }) 268 269 actual = xtest.MustPrettyJSONString(t, string(body)) 270 271 assert.Equal(t, expected, actual, 272 xtest.Diff(expected, actual)) 273 } 274 275 func TestValidateUpdateRequest(t *testing.T) { 276 var ( 277 reqEmptyName = &admin.NamespaceUpdateRequest{ 278 Options: &nsproto.NamespaceOptions{ 279 BootstrapEnabled: true, 280 }, 281 } 282 283 reqEmptyOptions = &admin.NamespaceUpdateRequest{ 284 Name: "foo", 285 } 286 287 reqNoNonZeroFields = &admin.NamespaceUpdateRequest{ 288 Name: "foo", 289 Options: &nsproto.NamespaceOptions{}, 290 } 291 292 reqNonZeroBootstrap = &admin.NamespaceUpdateRequest{ 293 Name: "foo", 294 Options: &nsproto.NamespaceOptions{ 295 RetentionOptions: &nsproto.RetentionOptions{ 296 BlockSizeNanos: 1, 297 }, 298 BootstrapEnabled: true, 299 }, 300 } 301 302 reqNonZeroBlockSize = &admin.NamespaceUpdateRequest{ 303 Name: "foo", 304 Options: &nsproto.NamespaceOptions{ 305 RetentionOptions: &nsproto.RetentionOptions{ 306 BlockSizeNanos: 1, 307 }, 308 }, 309 } 310 311 reqValid = &admin.NamespaceUpdateRequest{ 312 Name: "foo", 313 Options: &nsproto.NamespaceOptions{ 314 RetentionOptions: &nsproto.RetentionOptions{ 315 RetentionPeriodNanos: 1, 316 }, 317 }, 318 } 319 ) 320 321 for _, test := range []struct { 322 name string 323 request *admin.NamespaceUpdateRequest 324 expErr error 325 }{ 326 { 327 name: "emptyName", 328 request: reqEmptyName, 329 expErr: errEmptyNamespaceName, 330 }, 331 { 332 name: "emptyOptions", 333 request: reqEmptyOptions, 334 expErr: errEmptyNamespaceOptions, 335 }, 336 { 337 name: "emptyNoNonZeroFields", 338 request: reqNoNonZeroFields, 339 expErr: errEmptyNamespaceOptions, 340 }, 341 { 342 name: "nonZeroBootstrapField", 343 request: reqNonZeroBootstrap, 344 expErr: errNamespaceFieldImmutable, 345 }, 346 { 347 name: "nonZeroBlockSize", 348 request: reqNonZeroBlockSize, 349 expErr: errNamespaceFieldImmutable, 350 }, 351 { 352 name: "valid", 353 request: reqValid, 354 expErr: nil, 355 }, 356 } { 357 t.Run(test.name, func(t *testing.T) { 358 err := validateUpdateRequest(test.request) 359 if err != nil { 360 assert.True(t, errors.Is(err, test.expErr), 361 fmt.Sprintf("expected=%s, actual=%s", test.expErr, err)) 362 return 363 } 364 365 assert.NoError(t, err) 366 }) 367 } 368 }