diff --git a/CHANGELOG.md b/CHANGELOG.md index 33921bf4..08782ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added `AwarenessAttributeStats` to `/_cluster/health` ([#534](https://github.com/opensearch-project/opensearch-api-specification/pull/534)) - Added `cache_reserved_in_bytes` to `ClusterFileSystem` ([#534](https://github.com/opensearch-project/opensearch-api-specification/pull/534)) - Added `cluster_manager` to `ClusterNodeCount` ([#534](https://github.com/opensearch-project/opensearch-api-specification/pull/534)) +- Added request and response schemas for `/_cluster/routing/awareness/{attribute}/weights` ([#524](https://github.com/opensearch-project/opensearch-api-specification/pull/524)) ### Changed diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 98dae41a..6c38c31f 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -201,6 +201,8 @@ Consider the following chapters in [ml/model_groups](tests/plugins/ml/ml/model_g ``` As you can see, the `output` field in the first chapter saves the `model_group_id` from the response body. This value is then used in the subsequent chapters to query and delete the model group. +You can also supply defaults in both output and input values, e.g. `version: payload.version ? 0` or `${weights._version ? 0}` used in [cluster/routing/awareness/weights.yaml](tests/default/cluster/routing/awareness/weights.yaml). + You can also reuse output in payload expectations. See [tests/plugins/index_state_management/nodes/plugins/index_state_management.yaml](tests/plugins/index_state_management/nodes/plugins/index_state_management.yaml) for an example. ### Managing Versions diff --git a/spec/namespaces/cluster.yaml b/spec/namespaces/cluster.yaml index d526738d..12435950 100644 --- a/spec/namespaces/cluster.yaml +++ b/spec/namespaces/cluster.yaml @@ -195,6 +195,8 @@ paths: url: https://opensearch.org/docs/latest/api-reference/cluster-api/cluster-awareness/#example-weighted-round-robin-search parameters: - $ref: '#/components/parameters/cluster.put_weighted_routing::path.attribute' + requestBody: + $ref: '#/components/requestBodies/cluster.put_weighted_routing' responses: '200': $ref: '#/components/responses/cluster.put_weighted_routing@200' @@ -465,6 +467,11 @@ paths: $ref: '#/components/responses/cluster.remote_info@200' components: requestBodies: + cluster.put_weighted_routing: + content: + application/json: + schema: + $ref: '../schemas/cluster.weighted_routing.yaml#/components/schemas/Weights' cluster.allocation_explain: content: application/json: @@ -647,7 +654,11 @@ components: required: - persistent - transient - cluster.get_weighted_routing@200: {} + cluster.get_weighted_routing@200: + content: + application/json: + schema: + $ref: '../schemas/cluster.weighted_routing.yaml#/components/schemas/WeightsResponse' cluster.health@200: content: application/json: @@ -692,7 +703,11 @@ components: - acknowledged - persistent - transient - cluster.put_weighted_routing@200: {} + cluster.put_weighted_routing@200: + content: + application/json: + schema: + type: object cluster.remote_info@200: content: application/json: diff --git a/spec/schemas/cluster.weighted_routing.yaml b/spec/schemas/cluster.weighted_routing.yaml new file mode 100644 index 00000000..c4579ba8 --- /dev/null +++ b/spec/schemas/cluster.weighted_routing.yaml @@ -0,0 +1,23 @@ +openapi: 3.1.0 +info: + title: Schemas of Weighted Routing Category + description: Schemas of weighted routing category. + version: 1.0.0 +paths: {} +components: + schemas: + Weights: + type: object + properties: + _version: + $ref: '_common.yaml#/components/schemas/VersionNumber' + weights: + type: object + WeightsResponse: + allOf: + - $ref: '#/components/schemas/Weights' + - type: object + properties: + discovered_cluster_manager: + type: boolean + diff --git a/tests/default/cluster/routing/awareness/weights.yaml b/tests/default/cluster/routing/awareness/weights.yaml new file mode 100644 index 00000000..78837314 --- /dev/null +++ b/tests/default/cluster/routing/awareness/weights.yaml @@ -0,0 +1,36 @@ +$schema: ../../../../../json_schemas/test_story.schema.yaml + +description: Test cluster routing settings. +version: '>= 2.16' +prologues: + - path: /_cluster/settings + method: PUT + request: + payload: + transient: + cluster.routing.allocation.awareness.attributes: zone + cluster.routing.allocation.awareness.force.zone.values: + - zone-a + - zone-b +chapters: + - synopsis: Get zone routing weights. + id: weights + path: /_cluster/routing/awareness/{attribute}/weights + method: GET + parameters: + attribute: zone + output: + version: payload._version ? -1 + - synopsis: Update zone routing weights. + path: /_cluster/routing/awareness/{attribute}/weights + method: PUT + parameters: + attribute: zone + request: + payload: + weights: + zone-a: '1' + zone-b: '0' + _version: ${weights.version} + response: + status: 200 diff --git a/tools/src/tester/ChapterOutput.ts b/tools/src/tester/ChapterOutput.ts index d8fd6d11..e3c9ae85 100644 --- a/tools/src/tester/ChapterOutput.ts +++ b/tools/src/tester/ChapterOutput.ts @@ -35,7 +35,11 @@ export class ChapterOutput { if (response.payload === undefined) { return { evaluation: { result: Result.ERROR, message: 'No payload found in response, but expected output: ' + path } } } - value = _.get(response, path) + const rhs = path.replaceAll(' ', '').split('?', 2) + let default_value: any = parseFloat(rhs[1]) + if (Number.isNaN(default_value)) default_value = rhs[1] + value = _.get(response, rhs[0]) + if (value === undefined) value = default_value if (value === undefined) { return { evaluation: { result: Result.ERROR, message: `Expected to find non undefined value at \`${path}\`.` } } } diff --git a/tools/src/tester/StoryOutputs.ts b/tools/src/tester/StoryOutputs.ts index 41b8d9e7..08171e45 100644 --- a/tools/src/tester/StoryOutputs.ts +++ b/tools/src/tester/StoryOutputs.ts @@ -50,7 +50,7 @@ export class StoryOutputs { resolve_string (str: string): any { const ref = OutputReference.parse(str) if (ref) { - return this.get_output_value(ref.chapter_id, ref.output_name) + return this.get_output_value(ref.chapter_id, ref.output_name) ?? ref.default_value } else { return str } diff --git a/tools/src/tester/types/eval.types.ts b/tools/src/tester/types/eval.types.ts index eacb2c62..9474836c 100644 --- a/tools/src/tester/types/eval.types.ts +++ b/tools/src/tester/types/eval.types.ts @@ -87,15 +87,20 @@ export enum Result { export class OutputReference { chapter_id: string output_name: string - private constructor (chapter_id: string, output_name: string) { + default_value?: string + private constructor (chapter_id: string, output_name: string, default_value?: string) { this.chapter_id = chapter_id this.output_name = output_name + this.default_value = default_value } static parse (str: string): OutputReference | undefined { if (str.startsWith('${') && str.endsWith('}')) { - const spl = str.slice(2, -1).split('.', 2) - return { chapter_id: spl[0], output_name: spl[1] } + const spl = str.slice(2, -1).replaceAll(' ', '').split('.', 2) + const rhs = spl[1].split('?', 2) + let default_value: any = parseFloat(rhs[1]) + if (Number.isNaN(default_value)) default_value = rhs[1] + return { chapter_id: spl[0], output_name: rhs[0], default_value } } return undefined } diff --git a/tools/tests/tester/ChapterOutput.test.ts b/tools/tests/tester/ChapterOutput.test.ts index f5ace6ed..06be1bea 100644 --- a/tools/tests/tester/ChapterOutput.test.ts +++ b/tools/tests/tester/ChapterOutput.test.ts @@ -36,7 +36,8 @@ describe('with an object response', () => { { d: 2 }, { e: 3 } ] - } + }, + zero: 0 }) test('returns nested values', () => { @@ -68,6 +69,18 @@ describe('with an object response', () => { }) }) + test('uses a default value', () => { + expect(ChapterOutput.extract_output_values(response, { x: 'payload.a.b.x[0] ? -1' })).toEqual( + passed_output({ x: -1 }) + ) + }) + + test('does not use a default value on a numeric zero', () => { + expect(ChapterOutput.extract_output_values(response, { x: 'payload.zero ? -1' })).toEqual( + passed_output({ x: 0 }) + ) + }) + test('errors on invalid source', () => { expect(ChapterOutput.extract_output_values(response, { x: 'foobar' })).toEqual({ evaluation: { diff --git a/tools/tests/tester/StoryOutputs.test.ts b/tools/tests/tester/StoryOutputs.test.ts index 3017b7a6..450ddfd3 100644 --- a/tools/tests/tester/StoryOutputs.test.ts +++ b/tools/tests/tester/StoryOutputs.test.ts @@ -13,13 +13,18 @@ import { StoryOutputs } from 'tester/StoryOutputs' const story_outputs = new StoryOutputs({ chapter_id: new ChapterOutput({ x: 1, - y: 2 + y: 2, + n: null, + z: 0 }) }) test('resolve_string', () => { expect(story_outputs.resolve_string('${chapter_id.x}')).toEqual(1) expect(story_outputs.resolve_string('${invalid_id.x}')).toBeUndefined() + expect(story_outputs.resolve_string('${chapter_id.n ? x}')).toEqual('x') + expect(story_outputs.resolve_string('${chapter_id.n ? 0}')).toEqual(0) + expect(story_outputs.resolve_string('${chapter_id.z ? -1}')).toEqual(0) expect(story_outputs.resolve_string('some_str')).toEqual('some_str') })