Skip to content

Commit c6bb61e

Browse files
committed
resourcemanager: tighten controller config API checks
Signed-off-by: okjiang <819421878@qq.com>
1 parent f7de7cc commit c6bb61e

File tree

7 files changed

+131
-21
lines changed

7 files changed

+131
-21
lines changed

pkg/mcs/resourcemanager/metadataapi/config_service.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,11 @@ func (s *ConfigService) SetControllerConfig(c *gin.Context) {
253253
c.String(http.StatusForbidden, err.Error())
254254
return
255255
}
256-
c.String(http.StatusBadRequest, err.Error())
256+
if rmserver.IsControllerConfigValidationError(err) {
257+
c.String(http.StatusBadRequest, err.Error())
258+
return
259+
}
260+
c.String(http.StatusInternalServerError, err.Error())
257261
return
258262
}
259263
c.String(http.StatusOK, "Success!")

pkg/mcs/resourcemanager/metadataapi/config_service_test.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,36 @@ func TestConfigServiceControllerAllOrNothing(t *testing.T) {
9898
handler := newTestHTTPHandler(store)
9999

100100
resp := doJSONRequest(re, handler, http.MethodPost, "/resource-manager/api/v1/config/controller", map[string]any{
101-
"enable-controller-trace-log": true,
102-
"unknown": 1,
101+
"write-base-cost": 2.0,
102+
"unknown": 1,
103103
})
104104
re.Equal(http.StatusBadRequest, resp.Code)
105105
re.Empty(store.updatedControllerConfigItems)
106+
re.Zero(store.updateControllerConfigItemsCalls)
107+
re.Zero(store.updateControllerConfigItemCalls)
106108

107109
resp = doJSONRequest(re, handler, http.MethodPost, "/resource-manager/api/v1/config/controller", map[string]any{
108-
"enable-controller-trace-log": true,
110+
"write-base-cost": 2.0,
109111
})
110112
re.Equal(http.StatusOK, resp.Code)
111113
re.Len(store.updatedControllerConfigItems, 1)
114+
re.Equal([]string{"request-unit.write-base-cost"}, store.updatedControllerConfigItems)
115+
re.Equal(1, store.updateControllerConfigItemsCalls)
116+
re.Zero(store.updateControllerConfigItemCalls)
117+
}
118+
119+
func TestConfigServiceControllerStoreFailuresReturn500(t *testing.T) {
120+
t.Parallel()
121+
122+
re := require.New(t)
123+
store := newTestStore()
124+
store.updateControllerConfigItemsErr = errors.New("save controller config failed")
125+
handler := newTestHTTPHandler(store)
126+
127+
resp := doJSONRequest(re, handler, http.MethodPost, "/resource-manager/api/v1/config/controller", map[string]any{
128+
"write-base-cost": 2.0,
129+
})
130+
re.Equal(http.StatusInternalServerError, resp.Code)
112131
}
113132

114133
func TestConfigServiceKeyspaceServiceLimitAndErrors(t *testing.T) {
@@ -178,9 +197,12 @@ func TestConfigServiceMetadataWriteDisabledReturns403(t *testing.T) {
178197
re.Equal(http.StatusForbidden, resp.Code)
179198

180199
resp = doJSONRequest(re, handler, http.MethodPost, "/resource-manager/api/v1/config/controller", map[string]any{
181-
"enable-controller-trace-log": true,
200+
"write-base-cost": 1.0,
182201
})
183202
re.Equal(http.StatusForbidden, resp.Code)
203+
204+
resp = doJSONRequest(re, handler, http.MethodPost, "/resource-manager/api/v1/config/controller", map[string]any{})
205+
re.Equal(http.StatusOK, resp.Code)
184206
}
185207

186208
func newTestHTTPHandler(configStore ConfigStore) http.Handler {
@@ -209,13 +231,16 @@ func (*tokenOnlyManagerProvider) AddStartCallback(...func()) {}
209231
func (*tokenOnlyManagerProvider) AddServiceReadyCallback(...func(context.Context) error) {}
210232

211233
type testStore struct {
212-
keyspaceIDs map[string]uint32
213-
validKeyspaceIDs map[uint32]struct{}
214-
groups map[string]*rmserver.ResourceGroup
215-
serviceLimits map[uint32]float64
216-
addErr error
217-
setServiceLimitErr error
218-
updatedControllerConfigItems []string
234+
keyspaceIDs map[string]uint32
235+
validKeyspaceIDs map[uint32]struct{}
236+
groups map[string]*rmserver.ResourceGroup
237+
serviceLimits map[uint32]float64
238+
addErr error
239+
setServiceLimitErr error
240+
updateControllerConfigItemsErr error
241+
updatedControllerConfigItems []string
242+
updateControllerConfigItemsCalls int
243+
updateControllerConfigItemCalls int
219244
}
220245

221246
func newTestStore() *testStore {
@@ -288,13 +313,18 @@ func (*testStore) GetControllerConfig() *rmserver.ControllerConfig {
288313
}
289314

290315
func (s *testStore) UpdateControllerConfigItems(items map[string]any) error {
316+
s.updateControllerConfigItemsCalls++
317+
if s.updateControllerConfigItemsErr != nil {
318+
return s.updateControllerConfigItemsErr
319+
}
291320
for key := range items {
292321
s.updatedControllerConfigItems = append(s.updatedControllerConfigItems, key)
293322
}
294323
return nil
295324
}
296325

297326
func (s *testStore) UpdateControllerConfigItem(key string, _ any) error {
327+
s.updateControllerConfigItemCalls++
298328
s.updatedControllerConfigItems = append(s.updatedControllerConfigItems, key)
299329
return nil
300330
}

pkg/mcs/resourcemanager/server/apis/v1/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ func (s *Service) getControllerConfig(c *gin.Context) {
249249
// @Param config body object true "json params, rmserver.ControllerConfig"
250250
// @Success 200 {string} string "Success!"
251251
// @Failure 400 {string} error
252+
// @Failure 403 {string} error
253+
// @Failure 500 {string} error
252254
// @Router /config/controller [post]
253255
func (s *Service) setControllerConfig(c *gin.Context) {
254256
s.configService.SetControllerConfig(c)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026 TiKV Project Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package server
16+
17+
import (
18+
stderrors "errors"
19+
)
20+
21+
var errControllerConfigValidation = stderrors.New("controller config validation failed")
22+
23+
func wrapControllerConfigValidationError(err error) error {
24+
if err == nil {
25+
return nil
26+
}
27+
return &controllerConfigValidationError{err: err}
28+
}
29+
30+
// IsControllerConfigValidationError reports whether err is a controller-config validation error.
31+
func IsControllerConfigValidationError(err error) bool {
32+
return stderrors.Is(err, errControllerConfigValidation)
33+
}
34+
35+
type controllerConfigValidationError struct {
36+
err error
37+
}
38+
39+
func (e *controllerConfigValidationError) Error() string {
40+
return e.err.Error()
41+
}
42+
43+
func (e *controllerConfigValidationError) Unwrap() error {
44+
return e.err
45+
}
46+
47+
func (*controllerConfigValidationError) Is(target error) bool {
48+
return target == errControllerConfigValidation
49+
}

pkg/mcs/resourcemanager/server/manager.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ func (m *Manager) UpdateControllerConfigItem(key string, value any) error {
472472

473473
// UpdateControllerConfigItems updates controller config items atomically.
474474
func (m *Manager) UpdateControllerConfigItems(items map[string]any) error {
475+
if len(items) == 0 {
476+
return nil
477+
}
475478
if !m.writeRole.AllowsMetadataWrite() {
476479
return errMetadataWriteDisabled
477480
}
@@ -485,7 +488,7 @@ func (m *Manager) UpdateControllerConfigItems(items map[string]any) error {
485488
updated, err := applyControllerConfigItem(controllerConfig, key, value)
486489
if err != nil {
487490
m.Unlock()
488-
return err
491+
return wrapControllerConfigValidationError(err)
489492
}
490493
if updated {
491494
updatedItems = append(updatedItems, struct {

pkg/mcs/resourcemanager/server/manager_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,19 @@ func TestUpdateControllerConfigItemsAtomic(t *testing.T) {
620620
re.Equal(before.EnableControllerTraceLog, after.EnableControllerTraceLog)
621621
}
622622

623+
func TestUpdateControllerConfigValidationError(t *testing.T) {
624+
re := require.New(t)
625+
m := prepareManager()
626+
627+
ctx, cancel := context.WithCancel(context.Background())
628+
defer cancel()
629+
re.NoError(m.Init(ctx))
630+
631+
err := m.UpdateControllerConfigItem("ltb-max-wait-duration", "not-a-duration")
632+
re.Error(err)
633+
re.True(IsControllerConfigValidationError(err))
634+
}
635+
623636
func TestKeyspaceNameLookup(t *testing.T) {
624637
re := require.New(t)
625638
m := prepareManager()

tests/integrations/mcs/resourcemanager/api_test.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -340,15 +340,24 @@ func (suite *resourceManagerAPITestSuite) TestControllerConfigAPIAllOrNothing()
340340
re := suite.Require()
341341

342342
before := suite.mustGetControllerConfig(re)
343-
resp, statusCode := tryToSetControllerConfig(re, suite.cluster.GetLeaderServer().GetAddr(), map[string]any{
344-
"enable-controller-trace-log": "true",
345-
"ltb-max-wait-duration": "not-a-duration",
346-
})
347-
re.Equal(http.StatusBadRequest, statusCode)
348-
re.Contains(resp, "time:")
343+
payload := map[string]any{
344+
"read-base-cost": 2.0,
345+
"read-per-batch-base-cost": 3.0,
346+
"read-cost-per-byte": 4.0,
347+
"write-base-cost": 5.0,
348+
"write-cost-per-byte": 6.0,
349+
"ltb-max-wait-duration": "not-a-duration",
350+
}
351+
for i := 0; i < 8; i++ {
352+
// Old code updated fields one by one after decoding into a map, so repeating the
353+
// same mixed payload makes a partial write overwhelmingly likely if batching is gone.
354+
resp, statusCode := tryToSetControllerConfig(re, suite.cluster.GetLeaderServer().GetAddr(), payload)
355+
re.Equal(http.StatusBadRequest, statusCode)
356+
re.Contains(resp, "time:")
349357

350-
after := suite.mustGetControllerConfig(re)
351-
re.Equal(before, after)
358+
after := suite.mustGetControllerConfig(re)
359+
re.Equal(before, after)
360+
}
352361
}
353362

354363
func (suite *resourceManagerAPITestSuite) mustGetControllerConfig(re *require.Assertions) *server.ControllerConfig {

0 commit comments

Comments
 (0)