Skip to content

Commit 33e7fa0

Browse files
committed
Add OpenSearch 3.3.0+ API compatibility and enhance test infrastructure
OpenSearch 3.3.0+ API Compatibility: Add missing fields across multiple APIs for enhanced query failure tracking and system monitoring introduced in OpenSearch 3.3.0+: Nodes Stats API: - Add NeuralSearch breaker for neural search circuit breaker support - Add QueryFailed and StartreeQueryFailed for query failure tracking - Add SystemGeneratedProcessors and SystemGeneratedFactories to search pipeline for system component tracking Indices Stats API: - Add QueryFailed and StartreeQueryFailed search failure tracking fields Cat APIs (nodes, shards, indices): - Add SearchQueryFailed and SearchStartreeQueryFailed fields - Fix JSON field naming inconsistencies in cat indices API where OpenSearch uses mixed formats (pri.search.startree.query_* vs pri.search.startree_query_failed) Cluster State API: - Add IngestionStatus field to ClusterStateMetaDataIndex for tracking index ingestion status in cluster metadata Security Config API: - Add JwksURI field to JWT authentication configuration struct for JSON Web Key Set URI support Field Naming Compatibility Fix: OpenSearch 3.2.0 introduced inconsistent startree query field naming (pri.startree.* instead of pri.search.startree.*) which was corrected in 3.3.0+. 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. Test Infrastructure Enhancements: - Add comprehensive test infrastructure with automatic cluster readiness validation - Implement NewClient(t) with three-phase validation (health + cluster state + nodes info) - Add version-aware client configuration supporting both secure/insecure modes - Create testify-based OpenSearchTestSuite with built-in version detection - Implement robust readiness polling (25 attempts × 5s intervals) to eliminate CI flakiness - Add comprehensive test coverage for cat indices field consolidation compatibility - Update all integration tests to use enhanced test infrastructure These changes provide enhanced observability for query failures, neural search circuit breaker monitoring, search pipeline system components, index ingestion status tracking, JWT authentication configuration, field naming compatibility, and significantly improved test reliability across all OpenSearch versions. All compatibility code is marked for removal when 3.2.0 support ends. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent cc91806 commit 33e7fa0

58 files changed

Lines changed: 927 additions & 159 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-compatibility.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
matrix:
1111
secured: ["true", "false"]
1212
entry:
13-
- { opensearch_version: 1.3.18 }
13+
- { opensearch_version: 1.3.20 }
1414
- { opensearch_version: 2.0.1 }
1515
- { opensearch_version: 2.1.0 }
1616
- { opensearch_version: 2.2.1 }

CHANGELOG.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
55
## [Unreleased]
66

77
### Added
8+
- Enhanced cluster readiness checking for improved test reliability: `ostest.NewClient()` now includes readiness validation (health + cluster state + nodes info)
89

910
### Changed
1011
- Refactor Client struct to use embedded mutex pattern for improved thread safety ([#775](https://github.com/opensearch-project/opensearch-go/pull/775))
@@ -17,13 +18,13 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1718

1819
### Fixed
1920
- Fix flaky connection integration test by replacing arbitrary sleep times with proper server readiness polling
20-
- Fix nodes API compatibility with OpenSearch 3.1.0+ by adding phase_results_processors field
21-
- Fix cluster pending tasks API compatibility with OpenSearch 3.1.0+ by adding time_in_execution fields
22-
- Fix nodes stats API compatibility with OpenSearch 3.2.0+ by adding max_last_index_request_timestamp and startree query fields
23-
- Fix indices stats API compatibility with OpenSearch 3.2.0+ by adding max_last_index_request_timestamp and startree query fields
24-
- Fix cat APIs compatibility with OpenSearch 3.2.0+ by adding indexing timestamp and startree query fields
21+
- Fix OpenSearch 3.1.0+ API compatibility by adding phase_results_processors field to nodes API and time_in_execution fields to cluster pending tasks API
22+
- 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
23+
- 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
24+
- 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
25+
- 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
26+
- Fix cat APIs data type compatibility by changing byte fields from int to string to properly handle values like "0b"
2527
- Fix floating point precision loss in nodes stats concurrent_avg_slice_count field by changing from float32 to float64
26-
- Fix security plugin health API compatibility with OpenSearch 3.2.0+ by adding settings field
2728

2829
### Security
2930

internal/test/config.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// The OpenSearch Contributors require contributions made to
4+
// this file be licensed under the Apache-2.0 license or a
5+
// compatible open source license.
6+
7+
package ostest
8+
9+
import (
10+
"crypto/tls"
11+
"net/http"
12+
"os"
13+
14+
"github.com/opensearch-project/opensearch-go/v4"
15+
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
16+
)
17+
18+
// IsSecure returns true when SECURE_INTEGRATION env is set to true
19+
func IsSecure() bool {
20+
return os.Getenv("SECURE_INTEGRATION") == "true"
21+
}
22+
23+
// ClientConfig returns an opensearchapi.Config for both secure and insecure opensearch
24+
func ClientConfig() (*opensearchapi.Config, error) {
25+
if !IsSecure() {
26+
// For insecure integration tests, explicitly use HTTP
27+
return &opensearchapi.Config{
28+
Client: opensearch.Config{
29+
Addresses: []string{"http://localhost:9200"},
30+
},
31+
}, nil
32+
}
33+
34+
password, err := GetPassword()
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return &opensearchapi.Config{
40+
Client: opensearch.Config{
41+
Username: "admin",
42+
Password: password,
43+
Addresses: []string{"https://localhost:9200"},
44+
Transport: &http.Transport{
45+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
46+
},
47+
},
48+
}, nil
49+
}
50+
51+
// GetPassword returns the password suited for the opensearch version
52+
func GetPassword() (string, error) {
53+
var (
54+
major, minor int64
55+
err error
56+
)
57+
password := "admin"
58+
version := os.Getenv("OPENSEARCH_VERSION")
59+
60+
if version != "latest" && version != "" {
61+
major, minor, _, err = opensearch.ParseVersion(version)
62+
if err != nil {
63+
return "", err
64+
}
65+
if version == "latest" || major > 2 || (major == 2 && minor >= 12) {
66+
password = "myStrongPassword123!"
67+
}
68+
} else {
69+
password = "myStrongPassword123!"
70+
}
71+
return password, nil
72+
}

internal/test/doc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// The OpenSearch Contributors require contributions made to
4+
// this file be licensed under the Apache-2.0 license or a
5+
// compatible open source license.
6+
7+
// Package ostest provides OpenSearch testing utilities and infrastructure.
8+
// Integration-specific functionality is available only when building with integration tags.
9+
package ostest

internal/test/helper.go

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
// The OpenSearch Contributors require contributions made to
44
// this file be licensed under the Apache-2.0 license or a
55
// compatible open source license.
6+
//go:build integration
67

78
package ostest
89

910
import (
1011
"context"
11-
"crypto/tls"
1212
"encoding/json"
1313
"fmt"
1414
"io"
15-
"net/http"
1615
"os"
1716
"testing"
17+
"time"
1818

1919
"github.com/stretchr/testify/assert"
2020
"github.com/stretchr/testify/require"
@@ -25,71 +25,102 @@ import (
2525
)
2626

2727
// NewClient returns an opensearchapi.Client that is adjusted for the wanted test case
28-
func NewClient() (*opensearchapi.Client, error) {
28+
// and ensures the OpenSearch cluster is ready for requests.
29+
func NewClient(t *testing.T) (*opensearchapi.Client, error) {
30+
t.Helper()
2931
config, err := ClientConfig()
3032
if err != nil {
3133
return nil, err
3234
}
33-
if config != nil {
34-
return opensearchapi.NewClient(*config)
35+
36+
client, err := opensearchapi.NewClient(*config)
37+
if err != nil {
38+
return nil, err
3539
}
36-
return opensearchapi.NewDefaultClient()
37-
}
3840

39-
// IsSecure returns true when SECURE_INTEGRATION env is set to true
40-
func IsSecure() bool {
41-
return os.Getenv("SECURE_INTEGRATION") == "true"
41+
// Always wait for cluster readiness
42+
err = waitForClusterReady(t, client)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
return client, nil
4248
}
4349

44-
// ClientConfig returns an opensearchapi.Config for secure opensearch
45-
func ClientConfig() (*opensearchapi.Config, error) {
46-
if IsSecure() {
47-
password, err := GetPassword()
48-
if err != nil {
49-
return nil, err
50-
}
50+
// waitForClusterReady waits for the OpenSearch cluster to be fully ready for API calls.
51+
func waitForClusterReady(t *testing.T, client *opensearchapi.Client) error {
52+
t.Helper()
53+
const (
54+
maxAttempts = 25
55+
delayBetweenAttempts = 5 * time.Second
56+
requestTimeout = 2 * time.Second
57+
)
5158

52-
return &opensearchapi.Config{
53-
Client: opensearch.Config{
54-
Username: "admin",
55-
Password: password,
56-
Addresses: []string{"https://localhost:9200"},
57-
Transport: &http.Transport{
58-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
59-
},
60-
},
61-
}, nil
59+
// Get version for informational logging
60+
major, minor, patch, err := GetVersion(client, t)
61+
if err != nil {
62+
return fmt.Errorf("failed to get OpenSearch version: %w", err)
6263
}
63-
//nolint:nilnil // easier to test with nil rather then doing complex error handling for tests
64-
return nil, nil
65-
}
6664

67-
// GetPassword returns the password suited for the opensearch version
68-
func GetPassword() (string, error) {
69-
var (
70-
major, minor int64
71-
err error
72-
)
73-
password := "admin"
74-
version := os.Getenv("OPENSEARCH_VERSION")
65+
ctx, cancel := context.WithTimeout(t.Context(), requestTimeout)
66+
defer cancel()
7567

76-
if version != "latest" && version != "" {
77-
major, minor, _, err = opensearch.ParseVersion(version)
78-
if err != nil {
79-
return "", err
68+
for attempt := range maxAttempts {
69+
// Basic cluster health check
70+
resp, err := client.Cluster.Health(ctx, nil)
71+
if err != nil || resp == nil {
72+
t.Logf("Waiting %s for cluster readiness (attempt %d/%d)...", delayBetweenAttempts, attempt+1, maxAttempts)
73+
time.Sleep(delayBetweenAttempts)
74+
75+
// Reset context for next attempt
76+
ctx, cancel = context.WithTimeout(t.Context(), requestTimeout)
77+
defer cancel()
78+
continue
8079
}
81-
if version == "latest" || major > 2 || (major == 2 && minor >= 12) {
82-
password = "myStrongPassword123!"
80+
81+
// Extended readiness validation
82+
if err := extendedReadinessCheck(ctx, client); err == nil {
83+
if attempt > 0 {
84+
t.Logf("Cluster ready after %d attempts (version %d.%d.%d)", attempt+1, major, minor, patch)
85+
}
86+
return nil
8387
}
84-
} else {
85-
password = "myStrongPassword123!"
88+
89+
t.Logf("Cluster health OK but readiness validation failed (attempt %d/%d)", attempt+1, maxAttempts)
90+
time.Sleep(delayBetweenAttempts)
91+
92+
// Reset context for next attempt
93+
ctx, cancel = context.WithTimeout(t.Context(), requestTimeout)
94+
defer cancel()
8695
}
87-
return password, nil
96+
97+
return fmt.Errorf("cluster not ready after %d attempts (version %d.%d.%d)", maxAttempts, major, minor, patch)
98+
}
99+
100+
// extendedReadinessCheck performs validation checks to ensure the
101+
// cluster is ready
102+
func extendedReadinessCheck(ctx context.Context, client *opensearchapi.Client) error {
103+
// Try a simple cluster state request - this exercises more Java serialization paths
104+
_, err := client.Cluster.State(ctx, nil)
105+
if err != nil {
106+
return fmt.Errorf("cluster state check failed: %w", err)
107+
}
108+
109+
// Try a simple nodes info request - exercises node-level serialization
110+
_, err = client.Nodes.Info(ctx, nil)
111+
if err != nil {
112+
return fmt.Errorf("nodes info check failed: %w", err)
113+
}
114+
115+
return nil
88116
}
89117

90118
// GetVersion gets cluster info and returns version as int's
91-
func GetVersion(client *opensearchapi.Client) (int64, int64, int64, error) {
92-
resp, err := client.Info(context.Background(), nil)
119+
func GetVersion(client *opensearchapi.Client, t *testing.T) (int64, int64, int64, error) {
120+
if client == nil {
121+
return 0, 0, 0, fmt.Errorf("client cannot be nil")
122+
}
123+
resp, err := client.Info(t.Context(), nil)
93124
if err != nil {
94125
return 0, 0, 0, err
95126
}
@@ -99,22 +130,22 @@ func GetVersion(client *opensearchapi.Client) (int64, int64, int64, error) {
99130
// SkipIfBelowVersion skips a test if the cluster version is below a given version
100131
func SkipIfBelowVersion(t *testing.T, client *opensearchapi.Client, majorVersion, patchVersion int64, testName string) {
101132
t.Helper()
102-
major, patch, _, err := GetVersion(client)
133+
major, patch, _, err := GetVersion(client, t)
103134
assert.Nil(t, err)
104135
if major < majorVersion || (major == majorVersion && patch < patchVersion) {
105-
t.Skipf("Skiping %s as version %d.%d.x does not support this endpoint", testName, major, patch)
136+
t.Skipf("Skipping %s as version %d.%d.x does not support this endpoint", testName, major, patch)
106137
}
107138
}
108139

109-
// SkipIfNotSecure skips a test runs against an unsecure cluster
140+
// SkipIfNotSecure skips a test that runs against an insecure cluster
110141
func SkipIfNotSecure(t *testing.T) {
111142
t.Helper()
112143
if !IsSecure() {
113-
t.Skipf("Skiping %s as it needs a secured cluster", t.Name())
144+
t.Skipf("Skipping %s as it needs a secured cluster", t.Name())
114145
}
115146
}
116147

117-
// CompareRawJSONwithParsedJSON is a helper function to determin the difference between the parsed JSON and the raw JSON
148+
// CompareRawJSONwithParsedJSON is a helper function to determine the difference between the parsed JSON and the raw JSON
118149
// this is helpful to detect missing fields in the go structs
119150
func CompareRawJSONwithParsedJSON(t *testing.T, resp any, rawResp *opensearch.Response) {
120151
t.Helper()

0 commit comments

Comments
 (0)