Skip to content

Commit 1506bac

Browse files
committed
Fix cat indices API field naming compatibility across OpenSearch versions
OpenSearch 3.2.0 introduced inconsistent startree query field naming: - Used pri.startree.query_current instead of pri.search.startree.query_current - Used pri.startree.query_time instead of pri.search.startree.query_time - Used pri.startree.query_total instead of pri.search.startree.query_total OpenSearch 3.3.0+ corrected this back to the consistent naming pattern. This creates compatibility issues as applications need different field names depending on server version. Solution: Implement dual-field approach with automatic consolidation: - Stable fields use corrected 3.3.0+ naming as primary - V32 compatibility fields handle temporary 3.2.0 naming - consolidateV320StatsFields() method provides transparent fallback logic - Called automatically in catClient.Indices() after JSON unmarshaling Applications can use PrimarySearchStartreeQueryCurrent, PrimarySearchStartreeQueryTime, and PrimarySearchStartreeQueryTotal consistently regardless of OpenSearch server version. All compatibility code is marked for removal when 3.2.0 support ends. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 197a95a commit 1506bac

6 files changed

Lines changed: 119 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2020
- Fix OpenSearch 3.2.0+ API compatibility by adding max_last_index_request_timestamp and startree query fields across nodes stats, indices stats, and cat APIs, plus settings field to security plugin health API
2121
- Fix OpenSearch 3.3.0+ API compatibility by adding neural_search breaker, query_failed and startree_query_failed search fields, search pipeline system_generated fields across multiple APIs, plus ingestion_status field to cluster state API and jwks_uri field to security config API
2222
- Fix OpenSearch 3.4.0+ API compatibility by adding warmer fields to merges section, parallelism field to thread pool, and status_counter field across multiple APIs
23-
- Fix cat indices API field naming compatibility across OpenSearch versions by implementing field consolidation for startree query fields that changed naming format between 3.1.x and 3.2.0+
23+
- Fix cat indices API field naming compatibility across OpenSearch versions by using forward-compatible field names (PrimarySearchStartreeQuery*) that match the corrected 3.3.0+ naming, with fallback support for the temporary 3.2.0 field names
2424
- Fix cat APIs data type compatibility by changing byte fields from int to string to properly handle values like "0b"
2525
- Fix floating point precision loss in nodes stats concurrent_avg_slice_count field by changing from float32 to float64
2626

opensearchapi/api_cat-indices.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,35 @@ func (r CatIndicesResp) Inspect() Inspect {
239239
Response: r.response,
240240
}
241241
}
242+
243+
// consolidateV320StatsFields consolidates version-specific fields for all indices in the response.
244+
// This method should be called after JSON unmarshaling to ensure backward compatibility
245+
// across different OpenSearch server versions that may use different field names.
246+
//
247+
// COMPATIBILITY: This entire function exists only to handle OpenSearch 3.2.0's different
248+
// field naming (pri.startree.* instead of pri.search.startree.*). This function and all
249+
// V32 compatibility fields can be removed when OpenSearch 3.2.0 support is dropped.
250+
func (r *CatIndicesResp) consolidateV320StatsFields() {
251+
for i := range r.Indices {
252+
// PrimarySearchStartreeQueryCurrent: Fallback to V32 field if stable field is nil/zero
253+
if r.Indices[i].PrimarySearchStartreeQueryCurrent == nil || *r.Indices[i].PrimarySearchStartreeQueryCurrent == 0 {
254+
if r.Indices[i].PrimarySearchStartreeQueryCurrentV32 != nil {
255+
r.Indices[i].PrimarySearchStartreeQueryCurrent = r.Indices[i].PrimarySearchStartreeQueryCurrentV32
256+
}
257+
}
258+
259+
// PrimarySearchStartreeQueryTime: Fallback to V32 field if stable field is nil/empty
260+
if r.Indices[i].PrimarySearchStartreeQueryTime == nil || *r.Indices[i].PrimarySearchStartreeQueryTime == "" {
261+
if r.Indices[i].PrimarySearchStartreeQueryTimeV32 != nil {
262+
r.Indices[i].PrimarySearchStartreeQueryTime = r.Indices[i].PrimarySearchStartreeQueryTimeV32
263+
}
264+
}
265+
266+
// PrimarySearchStartreeQueryTotal: Fallback to V32 field if stable field is nil/empty
267+
if r.Indices[i].PrimarySearchStartreeQueryTotal == nil || *r.Indices[i].PrimarySearchStartreeQueryTotal == "" {
268+
if r.Indices[i].PrimarySearchStartreeQueryTotalV32 != nil {
269+
r.Indices[i].PrimarySearchStartreeQueryTotal = r.Indices[i].PrimarySearchStartreeQueryTotalV32
270+
}
271+
}
272+
}
273+
}

opensearchapi/api_cat-indices_consolidation_test.go

Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -14,144 +14,144 @@ import (
1414
)
1515

1616
// TestCatIndicesConsolidateFields tests the consolidateFields function
17-
// that handles version compatibility for OpenSearch field name changes
17+
// that handles version compatibility for OpenSearch field name changes between 3.2.0 and 3.3.0+
1818
func TestCatIndicesConsolidateFields(t *testing.T) {
19-
t.Run("ConsolidateFromV31Fields", func(t *testing.T) {
20-
// Create a response with V31 field values
19+
t.Run("StableFields3.3.0Plus", func(t *testing.T) {
20+
// Create a response with stable 3.3.0+ field values
2121
resp := CatIndicesResp{
2222
Indices: []CatIndexResp{
2323
{
2424
Index: "test-index",
2525
Health: "yellow",
2626
Status: "open",
27-
PrimaryStartreeQueryCurrentV31: intPtr(10),
28-
PrimaryStartreeQueryTimeV31: stringPtr("15s"),
29-
PrimaryStartreeQueryTotalV31: stringPtr("100"),
27+
PrimarySearchStartreeQueryCurrent: intPtr(10),
28+
PrimarySearchStartreeQueryTime: stringPtr("15s"),
29+
PrimarySearchStartreeQueryTotal: stringPtr("100"),
3030
},
3131
},
3232
}
3333

3434
// Call consolidateFields method
35-
resp.consolidateFields()
35+
resp.consolidateV320StatsFields()
3636

37-
// Check that V31 fields were consolidated into main fields
38-
assert.Equal(t, 10, *resp.Indices[0].PrimaryStartreeQueryCurrent)
39-
assert.Equal(t, "15s", *resp.Indices[0].PrimaryStartreeQueryTime)
40-
assert.Equal(t, "100", *resp.Indices[0].PrimaryStartreeQueryTotal)
37+
// Stable fields should remain unchanged
38+
assert.Equal(t, 10, *resp.Indices[0].PrimarySearchStartreeQueryCurrent)
39+
assert.Equal(t, "15s", *resp.Indices[0].PrimarySearchStartreeQueryTime)
40+
assert.Equal(t, "100", *resp.Indices[0].PrimarySearchStartreeQueryTotal)
4141
})
4242

43-
t.Run("ConsolidateFromV32Fields", func(t *testing.T) {
44-
// Create a response with V32 field values
43+
t.Run("FallbackToV32Fields", func(t *testing.T) {
44+
// Create a response with only V32 field values (3.2.0 compatibility)
4545
resp := CatIndicesResp{
4646
Indices: []CatIndexResp{
4747
{
48-
Index: "test-index",
49-
Health: "yellow",
50-
Status: "open",
51-
PrimaryStartreeQueryCurrentV32: intPtr(20),
52-
PrimaryStartreeQueryTimeV32: stringPtr("25s"),
53-
PrimaryStartreeQueryTotalV32: stringPtr("200"),
48+
Index: "test-index",
49+
Health: "yellow",
50+
Status: "open",
51+
PrimarySearchStartreeQueryCurrentV32: intPtr(20),
52+
PrimarySearchStartreeQueryTimeV32: stringPtr("25s"),
53+
PrimarySearchStartreeQueryTotalV32: stringPtr("200"),
5454
},
5555
},
5656
}
5757

58-
resp.consolidateFields()
58+
resp.consolidateV320StatsFields()
5959

60-
// Check that V32 fields were consolidated into main fields
61-
assert.Equal(t, 20, *resp.Indices[0].PrimaryStartreeQueryCurrent)
62-
assert.Equal(t, "25s", *resp.Indices[0].PrimaryStartreeQueryTime)
63-
assert.Equal(t, "200", *resp.Indices[0].PrimaryStartreeQueryTotal)
60+
// Should fallback to V32 values when stable fields are nil
61+
assert.Equal(t, 20, *resp.Indices[0].PrimarySearchStartreeQueryCurrent)
62+
assert.Equal(t, "25s", *resp.Indices[0].PrimarySearchStartreeQueryTime)
63+
assert.Equal(t, "200", *resp.Indices[0].PrimarySearchStartreeQueryTotal)
6464
})
6565

66-
t.Run("ConsolidatePreferV31OverV32", func(t *testing.T) {
67-
// Create a response with both V31 and V32 field values
66+
t.Run("StableFieldsTakePrecedence", func(t *testing.T) {
67+
// Create a response with both stable and V32 field values
6868
resp := CatIndicesResp{
6969
Indices: []CatIndexResp{
7070
{
71-
Index: "test-index",
72-
Health: "yellow",
73-
Status: "open",
74-
PrimaryStartreeQueryCurrentV31: intPtr(10),
75-
PrimaryStartreeQueryCurrentV32: intPtr(20),
76-
PrimaryStartreeQueryTimeV31: stringPtr("15s"),
77-
PrimaryStartreeQueryTimeV32: stringPtr("25s"),
78-
PrimaryStartreeQueryTotalV31: stringPtr("100"),
79-
PrimaryStartreeQueryTotalV32: stringPtr("200"),
71+
Index: "test-index",
72+
Health: "yellow",
73+
Status: "open",
74+
PrimarySearchStartreeQueryCurrent: intPtr(10),
75+
PrimarySearchStartreeQueryCurrentV32: intPtr(20),
76+
PrimarySearchStartreeQueryTime: stringPtr("15s"),
77+
PrimarySearchStartreeQueryTimeV32: stringPtr("25s"),
78+
PrimarySearchStartreeQueryTotal: stringPtr("100"),
79+
PrimarySearchStartreeQueryTotalV32: stringPtr("200"),
8080
},
8181
},
8282
}
8383

84-
resp.consolidateFields()
84+
resp.consolidateV320StatsFields()
8585

86-
// V31 should take precedence
87-
assert.Equal(t, 10, *resp.Indices[0].PrimaryStartreeQueryCurrent)
88-
assert.Equal(t, "15s", *resp.Indices[0].PrimaryStartreeQueryTime)
89-
assert.Equal(t, "100", *resp.Indices[0].PrimaryStartreeQueryTotal)
86+
// Stable fields should take precedence
87+
assert.Equal(t, 10, *resp.Indices[0].PrimarySearchStartreeQueryCurrent)
88+
assert.Equal(t, "15s", *resp.Indices[0].PrimarySearchStartreeQueryTime)
89+
assert.Equal(t, "100", *resp.Indices[0].PrimarySearchStartreeQueryTotal)
9090
})
9191

92-
t.Run("ConsolidateHandleZeroValues", func(t *testing.T) {
93-
// Create a response where V31 has zero values, should use V32
92+
t.Run("HandleZeroValuesInStableFields", func(t *testing.T) {
93+
// Create a response where stable fields have zero/empty values, should use V32
9494
resp := CatIndicesResp{
9595
Indices: []CatIndexResp{
9696
{
97-
Index: "test-index",
98-
Health: "yellow",
99-
Status: "open",
100-
PrimaryStartreeQueryCurrentV31: intPtr(0),
101-
PrimaryStartreeQueryCurrentV32: intPtr(20),
102-
PrimaryStartreeQueryTimeV31: stringPtr(""),
103-
PrimaryStartreeQueryTimeV32: stringPtr("25s"),
104-
PrimaryStartreeQueryTotalV31: stringPtr(""),
105-
PrimaryStartreeQueryTotalV32: stringPtr("200"),
97+
Index: "test-index",
98+
Health: "yellow",
99+
Status: "open",
100+
PrimarySearchStartreeQueryCurrent: intPtr(0),
101+
PrimarySearchStartreeQueryCurrentV32: intPtr(20),
102+
PrimarySearchStartreeQueryTime: stringPtr(""),
103+
PrimarySearchStartreeQueryTimeV32: stringPtr("25s"),
104+
PrimarySearchStartreeQueryTotal: stringPtr(""),
105+
PrimarySearchStartreeQueryTotalV32: stringPtr("200"),
106106
},
107107
},
108108
}
109109

110-
resp.consolidateFields()
110+
resp.consolidateV320StatsFields()
111111

112-
// Should use V32 values when V31 has zero/empty values
113-
assert.Equal(t, 20, *resp.Indices[0].PrimaryStartreeQueryCurrent)
114-
assert.Equal(t, "25s", *resp.Indices[0].PrimaryStartreeQueryTime)
115-
assert.Equal(t, "200", *resp.Indices[0].PrimaryStartreeQueryTotal)
112+
// Should use V32 values when stable fields have zero/empty values
113+
assert.Equal(t, 20, *resp.Indices[0].PrimarySearchStartreeQueryCurrent)
114+
assert.Equal(t, "25s", *resp.Indices[0].PrimarySearchStartreeQueryTime)
115+
assert.Equal(t, "200", *resp.Indices[0].PrimarySearchStartreeQueryTotal)
116116
})
117117

118-
t.Run("ConsolidateMultipleIndices", func(t *testing.T) {
119-
// Create a response with multiple indices
118+
t.Run("MultipleIndicesMixedVersions", func(t *testing.T) {
119+
// Create a response with multiple indices using different version formats
120120
resp := CatIndicesResp{
121121
Indices: []CatIndexResp{
122122
{
123123
Index: "test-index-1",
124124
Health: "yellow",
125125
Status: "open",
126-
PrimaryStartreeQueryCurrentV31: intPtr(10),
127-
PrimaryStartreeQueryTimeV31: stringPtr("15s"),
128-
PrimaryStartreeQueryTotalV31: stringPtr("100"),
126+
PrimarySearchStartreeQueryCurrent: intPtr(10),
127+
PrimarySearchStartreeQueryTime: stringPtr("15s"),
128+
PrimarySearchStartreeQueryTotal: stringPtr("100"),
129129
},
130130
{
131-
Index: "test-index-2",
132-
Health: "green",
133-
Status: "open",
134-
PrimaryStartreeQueryCurrentV32: intPtr(30),
135-
PrimaryStartreeQueryTimeV32: stringPtr("35s"),
136-
PrimaryStartreeQueryTotalV32: stringPtr("300"),
131+
Index: "test-index-2",
132+
Health: "green",
133+
Status: "open",
134+
PrimarySearchStartreeQueryCurrentV32: intPtr(30),
135+
PrimarySearchStartreeQueryTimeV32: stringPtr("35s"),
136+
PrimarySearchStartreeQueryTotalV32: stringPtr("300"),
137137
},
138138
},
139139
}
140140

141-
resp.consolidateFields()
141+
resp.consolidateV320StatsFields()
142142

143-
// Check first index (V31 fields)
144-
assert.Equal(t, 10, *resp.Indices[0].PrimaryStartreeQueryCurrent)
145-
assert.Equal(t, "15s", *resp.Indices[0].PrimaryStartreeQueryTime)
146-
assert.Equal(t, "100", *resp.Indices[0].PrimaryStartreeQueryTotal)
143+
// Check first index (stable fields)
144+
assert.Equal(t, 10, *resp.Indices[0].PrimarySearchStartreeQueryCurrent)
145+
assert.Equal(t, "15s", *resp.Indices[0].PrimarySearchStartreeQueryTime)
146+
assert.Equal(t, "100", *resp.Indices[0].PrimarySearchStartreeQueryTotal)
147147

148-
// Check second index (V32 fields)
149-
assert.Equal(t, 30, *resp.Indices[1].PrimaryStartreeQueryCurrent)
150-
assert.Equal(t, "35s", *resp.Indices[1].PrimaryStartreeQueryTime)
151-
assert.Equal(t, "300", *resp.Indices[1].PrimaryStartreeQueryTotal)
148+
// Check second index (V32 fallback)
149+
assert.Equal(t, 30, *resp.Indices[1].PrimarySearchStartreeQueryCurrent)
150+
assert.Equal(t, "35s", *resp.Indices[1].PrimarySearchStartreeQueryTime)
151+
assert.Equal(t, "300", *resp.Indices[1].PrimarySearchStartreeQueryTotal)
152152
})
153153

154-
t.Run("ConsolidateNilFields", func(t *testing.T) {
154+
t.Run("AllFieldsNil", func(t *testing.T) {
155155
// Test with nil fields (should not panic)
156156
resp := CatIndicesResp{
157157
Indices: []CatIndexResp{
@@ -165,12 +165,12 @@ func TestCatIndicesConsolidateFields(t *testing.T) {
165165
}
166166

167167
// Should not panic
168-
resp.consolidateFields()
168+
resp.consolidateV320StatsFields()
169169

170170
// All consolidated fields should remain nil
171-
assert.Nil(t, resp.Indices[0].PrimaryStartreeQueryCurrent)
172-
assert.Nil(t, resp.Indices[0].PrimaryStartreeQueryTime)
173-
assert.Nil(t, resp.Indices[0].PrimaryStartreeQueryTotal)
171+
assert.Nil(t, resp.Indices[0].PrimarySearchStartreeQueryCurrent)
172+
assert.Nil(t, resp.Indices[0].PrimarySearchStartreeQueryTime)
173+
assert.Nil(t, resp.Indices[0].PrimarySearchStartreeQueryTotal)
174174
})
175175
}
176176

opensearchapi/api_cat.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ func (c catClient) Indices(ctx context.Context, req *CatIndicesReq) (*CatIndices
130130
return &data, err
131131
}
132132

133+
// Consolidate version-specific fields to maintain backward compatibility
134+
data.consolidateV320StatsFields()
135+
133136
return &data, nil
134137
}
135138

opensearchtransport/connection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ func (cp *statusConnectionPool) URLs() []*url.URL {
239239
}
240240

241241
func (cp *statusConnectionPool) connections() []*Connection {
242-
var conns []*Connection
242+
conns := make([]*Connection, 0, len(cp.live)+len(cp.dead))
243243
conns = append(conns, cp.live...)
244244
conns = append(conns, cp.dead...)
245245

opensearchtransport/discovery_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ func TestDiscovery(t *testing.T) {
508508
}
509509
for _, tt := range tests {
510510
t.Run(tt.name, func(t *testing.T) {
511-
var urls []*url.URL
511+
urls := make([]*url.URL, 0, len(tt.args.Nodes))
512512
for _, node := range tt.args.Nodes {
513513
u, _ := url.Parse(node.URL)
514514
urls = append(urls, u)

0 commit comments

Comments
 (0)