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  }