Skip to content

Commit b55d650

Browse files
authored
Fb/unmanaged values 2 (#72)
* test for getRemoteFeaturesInfos * changeFeatureValue with clearSubScopes * better setup for changeFeatureValue with option clearSubScopes test * changeFeatureValue with option remoteOnly * test changeFeatureValue with option remoteOnly for failing case * feature key consistency fix * better test for remoteOnly cleanup * disable special markup markup for .http calls * wip service docs 1 * wip service docs 2 * make redisRead a read-privilege endpoint * add an example for redisUpdate with remoteOnly option * docs: add changeFeatureValue option remoteOnly * more wording on remoteOnly option * update webrick * more wording on remoteOnly option 2 * more information for read /state * more information for read /redis * more polish
1 parent 80c5426 commit b55d650

File tree

8 files changed

+358
-37
lines changed

8 files changed

+358
-37
lines changed

docs/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ GEM
289289
concurrent-ruby (~> 1.0)
290290
unicode-display_width (1.8.0)
291291
uri (0.13.1)
292-
webrick (1.8.1)
292+
webrick (1.8.2)
293293

294294
PLATFORMS
295295
aarch64-linux

docs/concepts/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ And we could change the behavior again with for the more specific user `john` an
8383

8484
- `changeFeatureValue(key, "new value just for john within t1", { user: "john", tenant: "t1" })`
8585

86-
{: .warn}
86+
{: .warn }
8787
As we can see in the precedence check order, if we had just set `changeFeatureValue(key, "new value for john", { user: "john" })`,
8888
then it depends on the order used in the `getFeatureValue` call, whether the `user` scope is evaluated before
8989
the `tenant` scope.

docs/plugin/index.md

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ It will usually be sufficient to set the `serviceAccessRoles` configuration, whi
4444
endpoints, but not the admin endpoints. If more discriminating access control is required, the `readAccessRoles` and
4545
`writeAccessRoles` can be set separately. For debugging purposes, you can also set the `adminAccessRoles`.
4646

47-
{: .warn}
47+
{: .warn }
4848
As the name suggests, the `adminAccessRoles` should be considered sensitive. It allows direct root access to the
4949
underlying redis.
5050

@@ -97,7 +97,7 @@ automatically detect it and configure it as follows:
9797
fallbackValue: false
9898
```
9999
100-
{: .info}
100+
{: .info }
101101
This automatic configuration can be _overwritten_, by using a configuration file and adding a dedicated configuration
102102
with the same key `/fts/my-feature`.
103103
@@ -109,14 +109,17 @@ for the related requests. For an example check out the [Example CAP Server](http
109109
This service endpoint will enable operations teams to understand toggle states. For practical requests, check the
110110
[http file](https://github.com/cap-js-community/feature-toggle-library/blob/main/example-cap-server/http/feature-service.http) in our example CAP Server.
111111
112-
### Read Feature Toggles State
112+
### Read Server State
113113
114-
Get all information about the current in-memory state of all toggles.
114+
Get information about the current in-memory state of all configured toggles. The response will give you transparency
115+
about maintained values and the underlying configuration of the toggles. In the following example, the
116+
`/check/priority` toggle has a maintained root value, and two scoped values. All other toggles have no maintained
117+
values, so they will use their fallback values.
115118
116119
<b>Example Request/Response</b>
117120
118121
- Request
119-
```http
122+
```
120123
GET /rest/feature/state
121124
Authorization: ...
122125
```
@@ -129,6 +132,11 @@ Get all information about the current in-memory state of all toggles.
129132
{
130133
"/check/priority": {
131134
"fallbackValue": 0,
135+
"rootValue": 1,
136+
"scopedValues": {
137+
"tenant::people": 10,
138+
"user::[email protected]": 100
139+
},
132140
"config": {
133141
"SOURCE": "FILE",
134142
"TYPE": "number",
@@ -168,19 +176,82 @@ Get all information about the current in-memory state of all toggles.
168176
}
169177
```
170178
179+
### Read Redis State
180+
181+
Get information about the remote redis state of all toggles with maintained values, even ones that are not configured.
182+
This endpoint will show the state within redis. Only toggles with maintained values will be shown here. In the example,
183+
we can see `/check/priority` with the maintained values. We can also see a `/legacy-key` toggle, which has maintained
184+
values that are not associated with a configuration `{ "SOURCE": "NONE" }`.
185+
186+
{: .info }
187+
Note that reading the redis state can reveal legacy key values that used to be configured and maintained but are no
188+
longer in the configuration. These values can be cleaned up by using the [redisUpdate](#update-feature-toggle) endpoint
189+
with the `remoteOnly` option.
190+
191+
<b>Example Request/Responses</b>
192+
193+
- Request
194+
```
195+
POST /rest/feature/redisRead
196+
Authorization: ...
197+
```
198+
- Response
199+
```
200+
HTTP/1.1 200 OK
201+
...
202+
```
203+
```json
204+
{
205+
"/check/priority": {
206+
"fallbackValue": 0,
207+
"rootValue": 1,
208+
"scopedValues": {
209+
"tenant::people": 10,
210+
"user::[email protected]": 100
211+
},
212+
"config": {
213+
"SOURCE": "FILE",
214+
"TYPE": "number",
215+
"VALIDATIONS": [
216+
{
217+
"scopes": ["user", "tenant"]
218+
},
219+
{
220+
"regex": "^\\d+$"
221+
},
222+
{
223+
"module": "$CONFIG_DIR/validators",
224+
"call": "validateTenantScope"
225+
}
226+
]
227+
}
228+
},
229+
"/legacy-key": {
230+
"rootValue": 10,
231+
"scopedValues": {
232+
"tenant::a": 100,
233+
"tenant::b": 1000
234+
},
235+
"config": {
236+
"SOURCE": "NONE"
237+
}
238+
}
239+
}
240+
```
241+
171242
## Service Endpoints for Write Privilege
172243
173244
Similar to the read privilege endpoints, these endpoints are meant to modify toggle state. For practical requests,
174245
check the [http file](https://github.com/cap-js-community/feature-toggle-library/blob/main/example-cap-server/http/feature-service.http) in our example CAP Server.
175246
176247
### Update Feature Toggle
177248
178-
Update the toggle state on Redis, which in turn is published to all server instances.
249+
Maintain a particular toggle value on Redis, which is automatically propagated to all server instances.
179250
180251
<b>Example Request/Responses</b>
181252
182253
- Valid Request
183-
```http
254+
```
184255
POST /rest/feature/redisUpdate
185256
Authorization: ...
186257
Content-Type: application/json
@@ -200,7 +271,7 @@ Update the toggle state on Redis, which in turn is published to all server insta
200271
```
201272
202273
- Valid Request with [clearSubScopes]({{ site.baseurl }}/usage/#updating-feature-value)
203-
```http
274+
```
204275
POST /rest/feature/redisUpdate
205276
Authorization: ...
206277
Content-Type: application/json
@@ -221,8 +292,31 @@ Update the toggle state on Redis, which in turn is published to all server insta
221292
...
222293
```
223294
295+
- Valid Request with [remoteOnly]({{ site.baseurl }}/usage/#updating-feature-value)
296+
```
297+
POST /rest/feature/redisUpdate
298+
Authorization: ...
299+
Content-Type: application/json
300+
```
301+
```json
302+
{
303+
"key": "/legacy-key",
304+
"value": null,
305+
"options": {
306+
"clearSubScopes": true,
307+
"remoteOnly": true
308+
}
309+
}
310+
```
311+
- Response
312+
313+
```
314+
HTTP/1.1 204 No Content
315+
...
316+
```
317+
224318
- Invalid Request
225-
```http
319+
```
226320
POST /rest/feature/redisUpdate
227321
Authorization: ...
228322
Content-Type: application/json
@@ -248,21 +342,6 @@ Update the toggle state on Redis, which in turn is published to all server insta
248342
}
249343
```
250344
251-
### Re-Sync Server with Redis
252-
253-
Force server to re-sync with Redis, this should never be necessary. It returns the same JSON structure as
254-
`/state`, after re-syncing.
255-
256-
<b>Example Request/Response</b>
257-
258-
- Request
259-
```http
260-
POST /rest/feature/redisRead
261-
Authorization: ...
262-
```
263-
- Response<br>
264-
Same as [Read Feature Toggles State](#read-feature-toggles-state).
265-
266345
## Service Endpoints for Admin Privilege
267346
268347
The service also offers an additional endpoint for deep problem analysis.
@@ -274,7 +353,7 @@ Send an arbitrary command to Redis. [https://redis.io/commands/](https://redis.i
274353
<b>Example Request/Responses</b>
275354
276355
- Request INFO
277-
```http
356+
```
278357
POST /rest/feature/redisSendCommand
279358
Authorization: ...
280359
Content-Type: application/json
@@ -298,7 +377,7 @@ Send an arbitrary command to Redis. [https://redis.io/commands/](https://redis.i
298377
...
299378
```
300379
- Request KEYS
301-
```http
380+
```
302381
POST /rest/feature/redisSendCommand
303382
Authorization: ...
304383
Content-Type: application/json

docs/usage/index.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,10 @@ processing delay until the change is picked up by all subscribers.
302302
Setting a feature value to `null` will delete the associated remote state and effectively reset it to its fallback
303303
value.
304304

305+
_Option: clearSubScopes_
306+
305307
Since setting values for scope-combinations happens additively, it can become hard to keep track of which combinations
306-
have dedicated values attached to them. If you want to set a value _and_ make sure that there isn't a more specific
308+
have maintained values attached to them. If you want to set a value _and_ make sure that there isn't a more specific
307309
scope-combination, which overrides that value, then you can use the option `{ clearSubScopes: true }` as a third
308310
argument. For example
309311

@@ -314,6 +316,24 @@ await toggles.changeFeatureValue("/srv/util/logger/logLevel", "error", {}, { cle
314316
will set the root-scope value to `"error"` and remove all sub-scopes. See
315317
[scoping]({{ site.baseurl }}/concepts/#scoping) for context.
316318

319+
_Option: remoteOnly_
320+
321+
When you find toggle values in Redis that are not configured, marked with `{ "SOURCE": "NONE" }`, it usually makes
322+
sense to remove them. In this situation, we want to change _just Redis_ and bypass the local server state update, the
323+
usual validation, change handlers, and server instance change propagation. To achieve this, you can use the
324+
`{ remoteOnly: true }` option. For example
325+
326+
```javascript
327+
await toggles.changeFeatureValue("/legacy-key", null, {}, { clearSubScopes: true, remoteOnly: true });
328+
```
329+
330+
will remove all maintained values associated with the `/legacy-key` key in Redis.
331+
332+
{: .info }
333+
Changes with the `{ remoteOnly: true }` option will be blocked for _configured_ toggles. This happens to avoid
334+
situations where the remote state of these toggles is accidentally changed in a way that bypasses validations and
335+
server state updates.
336+
317337
### Resetting Feature Value
318338

319339
There is a convenience reset API just to reset a feature toggle and remove all associated persisted values. Reading

src/plugin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const ACCESS = Object.freeze({
2525
ADMIN: "ADMIN",
2626
});
2727
const SERVICE_ENDPOINTS = Object.freeze({
28-
[ACCESS.READ]: [`${SERVICE_NAME}.state`],
29-
[ACCESS.WRITE]: [`${SERVICE_NAME}.redisRead`, `${SERVICE_NAME}.redisUpdate`],
28+
[ACCESS.READ]: [`${SERVICE_NAME}.state`, `${SERVICE_NAME}.redisRead`],
29+
[ACCESS.WRITE]: [`${SERVICE_NAME}.redisUpdate`],
3030
[ACCESS.ADMIN]: [`${SERVICE_NAME}.redisSendCommand`],
3131
});
3232

test/__mocks__/redisWrapper.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@ const getObject = jest.fn(async (key) => {
1010
return mockRedisState.values[key];
1111
});
1212

13+
const hashGetAllObjects = jest.fn(async () => {
14+
mockRedisState.values = mockRedisState.values ? mockRedisState.values : {};
15+
return mockRedisState.values[redisKey];
16+
});
17+
1318
const type = jest.fn(async () => "hash");
1419

1520
const watchedHashGetSetObject = jest.fn(async (key, field, newValueCallback) => {
1621
mockRedisState.values = mockRedisState.values ? mockRedisState.values : {};
1722
mockRedisState.values[key] = mockRedisState.values[key] ? mockRedisState.values[key] : {};
18-
mockRedisState.values[key][field] = await newValueCallback(mockRedisState.values[key][field]);
19-
return mockRedisState.values[key][field];
23+
const newValue = await newValueCallback(mockRedisState.values[key][field]);
24+
if (newValue === null) {
25+
Reflect.deleteProperty(mockRedisState.values[key], field);
26+
} else {
27+
mockRedisState.values[key][field] = newValue;
28+
}
29+
return newValue;
2030
});
2131

2232
const registerMessageHandler = jest.fn((channel, handler) => {
@@ -56,6 +66,7 @@ module.exports = {
5666
publishMessage,
5767
type,
5868
getObject,
69+
hashGetAllObjects,
5970
watchedHashGetSetObject,
6071
subscribe: jest.fn(),
6172
getIntegrationMode: jest.fn(() => REDIS_INTEGRATION_MODE.CF_REDIS),

test/__snapshots__/featureToggles.test.js.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ exports[`feature toggles test basic apis getFeaturesInfos 1`] = `
123123
}
124124
`;
125125

126+
exports[`feature toggles test basic apis getRemoteFeaturesInfos 1`] = `
127+
{
128+
"legacy-key": {
129+
"config": {
130+
"SOURCE": "NONE",
131+
},
132+
"rootValue": "legacy-root",
133+
"scopedValues": {
134+
"tenant::a": "legacy-scoped-value",
135+
},
136+
},
137+
"test/feature_b": {
138+
"config": {
139+
"SOURCE": "RUNTIME",
140+
"TYPE": "number",
141+
},
142+
"fallbackValue": 1,
143+
"rootValue": 1,
144+
"scopedValues": {
145+
"tenant::a": 10,
146+
},
147+
},
148+
}
149+
`;
150+
126151
exports[`feature toggles test basic apis initializeFeatureToggles 1`] = `
127152
{
128153
"test/feature_a": {

0 commit comments

Comments
 (0)