Skip to content

Commit a3de14c

Browse files
committed
Refactor test infrastructure and consolidate test utilities
Modernize the test infrastructure by consolidating test helpers into a shared testutil package and enhancing connection reliability: - Move test utilities from internal/test to opensearchutil/testutil for broader reusability across the project and external packages - Remove obsolete internal/test/config.go in favor of improved helper functions with better error handling and connection management - Add dynamic field filtering for JSON comparison tests to handle version-specific and environment-dependent OpenSearch responses - Enhance connection robustness with improved readiness checks and health monitoring in opensearchtransport layer - Update all integration tests across opensearchapi, plugins, and transport packages to use the new unified test infrastructure - Add comprehensive documentation and examples for the new test utilities This refactor provides a more maintainable foundation for testing across different OpenSearch versions and environments while reducing code duplication and improving test reliability. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 05d9979 commit a3de14c

99 files changed

Lines changed: 2928 additions & 2986 deletions

File tree

Some content is hidden

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

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
with:
1919
version: v2.8.0
2020
only-new-issues: true
21-
args: --fix --build-tags "integration core"
21+
args: --fix --build-tags "integration core plugins plugin_security plugin_index_management multinode"
2222

2323
prettify:
2424
name: Prettify

CHANGELOG.md

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

77
### Added
88

9-
- Enhanced cluster readiness checking for improved test reliability: `ostest.NewClient()` now includes readiness validation (health + cluster state + nodes info)
9+
- Enhanced cluster readiness checking for improved test reliability: `testutil.NewClient()` now includes readiness validation (health + cluster state + nodes info)
1010
- Test parallelization support via TEST_PARALLEL environment variable (default: CPU cores - 1, minimum 1)
1111
- opensearchutil/testutil package with PollUntil helper for eventual consistency testing (ISM policies, index readiness, cluster state changes)
1212
- Configuration option `IncludeDedicatedClusterManagers` for controlling cluster manager node routing ([#765](https://github.com/opensearch-project/opensearch-go/issues/765))
@@ -44,6 +44,12 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
4444
- `opensearch.Config` and `opensearchtransport.Config` now accept optional `Context` and `CancelFunc` fields
4545
- `opensearchutil.BulkIndexerConfig` now accepts optional `Context` and `CancelFunc` fields
4646
- Enables proper context propagation for timeouts, cancellation, and graceful shutdown
47+
- Role compatibility validation prevents conflicting role assignments (master+cluster_manager, warm+search)
48+
- OpenSearch 3.0+ searchable snapshots now use `warm` role instead of deprecated `search` role
49+
- **BREAKING**: Migrate `signer/aws` package from AWS SDK v1 to AWS SDK v2 due to AWS SDK v1 reaching end-of-support on July 31, 2025
50+
- Constructor now takes `aws.Config` instead of `session.Options`
51+
- See USER_GUIDE.md for details required to migrate
52+
- Users who need access to the existing `signer/awsv2` API can still use it, however they are encouraged to migrate to `signer/aws`
4753

4854
### Deprecated
4955

@@ -323,7 +329,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
323329

324330
- Updates workflow action versions ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
325331
- Changes integration tests to work with secure and unsecure OpenSearch ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
326-
- Moves functions from `opensearch/internal/test` to `internal/test` for more general test uses ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
332+
- Moves functions from `opensearch/internal/test` to `opensearchutil/testutil` for shared test utilities ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
327333
- Changes `custom_foldername` field to pointer as it can be `null` ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
328334
- Changs cat indices Primary and Replica field to pointer as it can be `null` ([#488](https://github.com/opensearch-project/opensearch-go/pull/488))
329335
- Replaces `ioutil` with `io` in examples and integration tests [#495](https://github.com/opensearch-project/opensearch-go/pull/495)

Makefile

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ cluster.build:
230230

231231
cluster.start:
232232
@$(MAKE) cluster.docker-up
233+
@$(MAKE) cluster.wait-ready
233234
@$(MAKE) cluster.get-cert
234235

235236
cluster.stop:
@@ -238,6 +239,7 @@ cluster.stop:
238239
cluster.docker-build:
239240
@# Determine version-specific settings
240241
$(eval OPENSEARCH_VERSION ?= latest)
242+
$(eval SECURE_INTEGRATION ?= false)
241243
$(eval version_major := $(shell \
242244
if [ "$(OPENSEARCH_VERSION)" = "latest" ]; then \
243245
echo "2"; \
@@ -252,13 +254,17 @@ cluster.docker-build:
252254
echo "cluster_manager"; \
253255
fi \
254256
))
255-
@echo "Building OpenSearch $(OPENSEARCH_VERSION) with role: $(manager_role)"
256-
OPENSEARCH_MANAGER_ROLE=$(manager_role) OPENSEARCH_MANAGER_SETTING=$(manager_role) \
257-
docker compose --project-directory .ci/opensearch build --pull
257+
@echo "Building OpenSearch $(OPENSEARCH_VERSION) with role: $(manager_role), secure: $(SECURE_INTEGRATION)"
258+
export SECURE_INTEGRATION=$(SECURE_INTEGRATION); \
259+
export OPENSEARCH_VERSION=$(OPENSEARCH_VERSION); \
260+
export OPENSEARCH_MANAGER_ROLE=$(manager_role); \
261+
export OPENSEARCH_MANAGER_SETTING=$(manager_role); \
262+
docker compose --project-directory .ci/opensearch build --pull
258263

259264
cluster.docker-up:
260265
@# Determine version-specific settings
261266
$(eval OPENSEARCH_VERSION ?= latest)
267+
$(eval SECURE_INTEGRATION ?= false)
262268
$(eval version_major := $(shell \
263269
if [ "$(OPENSEARCH_VERSION)" = "latest" ]; then \
264270
echo "2"; \
@@ -273,9 +279,12 @@ cluster.docker-up:
273279
echo "cluster_manager"; \
274280
fi \
275281
))
276-
@echo "Starting OpenSearch $(OPENSEARCH_VERSION) with role: $(manager_role)"
277-
OPENSEARCH_MANAGER_ROLE=$(manager_role) OPENSEARCH_MANAGER_SETTING=$(manager_role) \
278-
docker compose --project-directory .ci/opensearch up -d
282+
@echo "Starting OpenSearch $(OPENSEARCH_VERSION) with role: $(manager_role), secure: $(SECURE_INTEGRATION)"
283+
export SECURE_INTEGRATION=$(SECURE_INTEGRATION); \
284+
export OPENSEARCH_VERSION=$(OPENSEARCH_VERSION); \
285+
export OPENSEARCH_MANAGER_ROLE=$(manager_role); \
286+
export OPENSEARCH_MANAGER_SETTING=$(manager_role); \
287+
docker compose --project-directory .ci/opensearch up -d
279288

280289
cluster.scale.1: ## Start single-node cluster
281290
docker compose --project-directory .ci/opensearch up -d --scale opensearch-node2=0 --scale opensearch-node3=0;
@@ -297,6 +306,72 @@ cluster.get-cert:
297306
docker cp $$CONTAINER:/usr/share/opensearch/config/kirk-key.pem admin.key; \
298307
fi
299308

309+
cluster.wait-ready: ## Poll cluster until health status is green or yellow
310+
@printf "\033[2m-> Waiting for cluster to be ready...\033[0m\n"
311+
@{ \
312+
set -e; \
313+
MAX_ATTEMPTS=60; \
314+
ATTEMPT=1; \
315+
HTTP_URL="http://localhost:9200/_cluster/health"; \
316+
HTTPS_URL="https://localhost:9200/_cluster/health"; \
317+
HEALTH_URL=""; \
318+
CURL_OPTS=""; \
319+
VERSION="$${OPENSEARCH_VERSION:-latest}"; \
320+
if [ "$$VERSION" = "latest" ]; then \
321+
PASSWORD="myStrongPassword123!"; \
322+
else \
323+
MAJOR=$$(echo "$$VERSION" | cut -d. -f1); \
324+
MINOR=$$(echo "$$VERSION" | cut -d. -f2); \
325+
if [ $$MAJOR -gt 2 ] || ([ $$MAJOR -eq 2 ] && [ $$MINOR -ge 12 ]); then \
326+
PASSWORD="myStrongPassword123!"; \
327+
else \
328+
PASSWORD="admin"; \
329+
fi; \
330+
fi; \
331+
while [ $$ATTEMPT -le $$MAX_ATTEMPTS ]; do \
332+
if [ -z "$$HEALTH_URL" ]; then \
333+
if curl -sf "$$HTTP_URL" > /dev/null 2>&1; then \
334+
printf "\033[36m→ Detected insecure cluster (HTTP)\033[0m\n"; \
335+
HEALTH_URL="$$HTTP_URL"; \
336+
CURL_OPTS=""; \
337+
elif curl -sf -k -u "admin:$$PASSWORD" "$$HTTPS_URL" > /dev/null 2>&1; then \
338+
printf "\033[36m→ Detected secure cluster (HTTPS)\033[0m\n"; \
339+
HEALTH_URL="$$HTTPS_URL"; \
340+
CURL_OPTS="-k -u admin:$$PASSWORD"; \
341+
else \
342+
printf "\033[33m⋯ Waiting for cluster to respond (attempt $$ATTEMPT/$$MAX_ATTEMPTS)\033[0m\n"; \
343+
ATTEMPT=$$((ATTEMPT + 1)); \
344+
sleep 2; \
345+
continue; \
346+
fi; \
347+
fi; \
348+
if curl -sf $$CURL_OPTS "$$HEALTH_URL" > /dev/null 2>&1; then \
349+
STATUS=$$(curl -sf $$CURL_OPTS "$$HEALTH_URL" | grep -o '"status":"[^"]*"' | cut -d'"' -f4); \
350+
if [ "$$STATUS" = "green" ] || [ "$$STATUS" = "yellow" ]; then \
351+
printf "\033[32m✓ Cluster is ready (status: $$STATUS) after $$ATTEMPT attempts\033[0m\n"; \
352+
exit 0; \
353+
fi; \
354+
printf "\033[33m⋯ Cluster status: $$STATUS (attempt $$ATTEMPT/$$MAX_ATTEMPTS)\033[0m\n"; \
355+
else \
356+
printf "\033[33m⋯ Waiting for cluster to respond (attempt $$ATTEMPT/$$MAX_ATTEMPTS)\033[0m\n"; \
357+
fi; \
358+
ATTEMPT=$$((ATTEMPT + 1)); \
359+
sleep 2; \
360+
done; \
361+
printf "\033[31m✗ Cluster failed to become ready after $$MAX_ATTEMPTS attempts\033[0m\n"; \
362+
printf "\033[2m\n--- Diagnostic Information ---\033[0m\n"; \
363+
printf "\033[2mDocker containers:\033[0m\n"; \
364+
docker compose --project-directory .ci/opensearch ps || true; \
365+
printf "\033[2m\nRecent logs from containers:\033[0m\n"; \
366+
docker compose --project-directory .ci/opensearch logs --tail=50 || true; \
367+
printf "\033[2m\nAttempted URLs:\033[0m\n"; \
368+
printf " HTTP: $$HTTP_URL\n"; \
369+
printf " HTTPS: $$HTTPS_URL\n"; \
370+
printf "\033[2m\nCurl test results:\033[0m\n"; \
371+
printf " HTTP: "; curl -sf "$$HTTP_URL" && echo "✓ OK" || echo "✗ Failed"; \
372+
printf " HTTPS: "; curl -sf -k -u "admin:$$PASSWORD" "$$HTTPS_URL" && echo "✓ OK" || echo "✗ Failed"; \
373+
exit 1; \
374+
}
300375

301376
cluster.clean: ## Remove unused Docker volumes and networks
302377
@printf "\033[2m-> Cleaning up Docker assets...\033[0m\n"

USER_GUIDE.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,19 @@ Before starting, we strongly recommend reading the full AWS documentation regard
221221
>
222222
> See [Managed Domains signing-service requests.](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html#managedomains-signing-service-requests)
223223
224-
Depending on the version of AWS SDK used, import the v1 or v2 request signer from `signer/aws` or `signer/awsv2` respectively. Both signers are equivalent in their functionality, they provide AWS Signature Version 4 (SigV4).
224+
Depending on the version of AWS SDK used, import the request signer from `signer/aws` (recommended) or `signer/awsv2`. Both signers use AWS SDK v2 and provide AWS Signature Version 4 (SigV4).
225+
226+
**BREAKING CHANGE**: As of this version, the main `signer/aws` package has been migrated from AWS SDK v1 to AWS SDK v2 due to AWS SDK v1 reaching end-of-support on July 31, 2025.
225227

226228
To read more about SigV4 see [Signature Version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
227229

228230
Here are some Go samples that show how to sign each OpenSearch request and automatically search for AWS credentials from the ~/.aws folder or environment variables:
229231

230-
### AWS SDK v1
232+
### AWS SDK v2 (Recommended)
233+
234+
**Migration Note**: If you were previously using `signer/aws` with AWS SDK v1, you need to update your imports and configuration as shown below.
235+
236+
**Credential Caching**: The signer automatically enables credential caching for improved performance, especially when using STS credentials (assume role, web identity, etc.). This reduces API calls to AWS STS and improves signing performance.
231237

232238
```go
233239
package main
@@ -237,7 +243,8 @@ import (
237243
"fmt"
238244
"os"
239245

240-
"github.com/aws/aws-sdk-go/aws/session"
246+
"github.com/aws/aws-sdk-go-v2/aws"
247+
"github.com/aws/aws-sdk-go-v2/config"
241248
requestsigner "github.com/opensearch-project/opensearch-go/v4/signer/aws"
242249

243250
"github.com/opensearch-project/opensearch-go/v4"
@@ -256,16 +263,22 @@ func main() {
256263
const endpoint = "" // e.g. https://opensearch-domain.region.com
257264

258265
func example() error {
259-
// Create an AWS request Signer and load AWS configuration using default config folder or env vars.
260-
// See https://docs.aws.amazon.com/opensearch-service/latest/developerguide/request-signing.html#request-signing-go
261-
signer, err := requestsigner.NewSignerWithService(
262-
session.Options{SharedConfigState: session.SharedConfigEnable},
263-
requestsigner.OpenSearchService, // Use requestsigner.OpenSearchServerless for Amazon OpenSearch Serverless.
264-
)
266+
ctx := context.Background()
267+
268+
// Load AWS configuration
269+
awsCfg, err := config.LoadDefaultConfig(ctx)
265270
if err != nil {
266271
return err
267272
}
268-
// Create an opensearch client and use the request-signer.
273+
274+
// Create an AWS request Signer
275+
signer, err := requestsigner.NewSignerWithService(awsCfg, requestsigner.OpenSearchService)
276+
// Use requestsigner.OpenSearchServerless for Amazon OpenSearch Serverless
277+
if err != nil {
278+
return err
279+
}
280+
281+
// Create an opensearch client and use the request-signer
269282
client, err := opensearchapi.NewClient(
270283
opensearchapi.Config{
271284
Client: opensearch.Config{
@@ -278,20 +291,18 @@ func example() error {
278291
return err
279292
}
280293

281-
ctx := context.Background()
282-
283294
ping, err := client.Ping(ctx, nil)
284295
if err != nil {
285296
return err
286297
}
287298

288-
fmt.Println(ping)
299+
fmt.Println(ping)
289300

290301
return nil
291302
}
292303
```
293304

294-
### AWS SDK v2
305+
### Alternative: Using signer/awsv2
295306

296307
Use the AWS SDK v2 for Go to authenticate with Amazon OpenSearch service.
297308

error_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func TestError(t *testing.T) {
5050
assert.True(t, resp.IsError())
5151
err := opensearch.ParseError(resp)
5252
var testError *opensearch.StructError
53-
require.True(t, errors.As(err, &testError))
53+
require.ErrorAs(t, err, &testError)
5454
assert.Equal(t, http.StatusBadRequest, testError.Status)
5555
assert.Equal(t, "resource_already_exists_exception", testError.Err.Type)
5656
assert.Equal(t, "index [test/HU2mN_RMRXGcS38j3yV-VQ] already exists", testError.Err.Reason)
@@ -92,7 +92,7 @@ func TestError(t *testing.T) {
9292
assert.True(t, resp.IsError())
9393
err := opensearch.ParseError(resp)
9494
var testError *opensearch.StructError
95-
require.True(t, errors.As(err, &testError))
95+
require.ErrorAs(t, err, &testError)
9696
assert.Equal(t, http.StatusBadRequest, testError.Status)
9797
assert.Equal(t, "illegal_argument_exception", testError.Err.Type)
9898
assert.Equal(t, "composable template [posts] template after composition is invalid", testError.Err.Reason)
@@ -117,14 +117,14 @@ func TestError(t *testing.T) {
117117
}`),
118118
)
119119
body, err := io.ReadAll(reader)
120-
require.Nil(t, err)
120+
require.NoError(t, err)
121121

122122
var errStruct *opensearch.StructError
123123
err = json.Unmarshal(body, &errStruct)
124-
assert.NotNil(t, err)
124+
assert.Error(t, err)
125125

126126
var jsonError *json.UnmarshalTypeError
127-
assert.True(t, errors.As(err, &jsonError))
127+
assert.ErrorAs(t, err, &jsonError)
128128
})
129129
t.Run("string", func(t *testing.T) {
130130
reader := io.NopCloser(
@@ -134,14 +134,14 @@ func TestError(t *testing.T) {
134134
}`),
135135
)
136136
body, err := io.ReadAll(reader)
137-
require.Nil(t, err)
137+
require.NoError(t, err)
138138

139139
var errStruct *opensearch.StructError
140140
err = json.Unmarshal(body, &errStruct)
141-
assert.NotNil(t, err)
141+
assert.Error(t, err)
142142

143143
var errStr *opensearch.StringError
144-
require.True(t, errors.As(err, &errStr))
144+
require.ErrorAs(t, err, &errStr)
145145
})
146146
})
147147
})
@@ -159,7 +159,7 @@ func TestError(t *testing.T) {
159159
assert.True(t, resp.IsError())
160160
err := opensearch.ParseError(resp)
161161
var testError *opensearch.StringError
162-
require.True(t, errors.As(err, &testError))
162+
require.ErrorAs(t, err, &testError)
163163
assert.Equal(t, http.StatusMethodNotAllowed, testError.Status)
164164
assert.Contains(t, testError.Err, "Incorrect HTTP method for uri")
165165
_ = fmt.Sprintf("%s", testError)
@@ -175,7 +175,7 @@ func TestError(t *testing.T) {
175175
assert.True(t, resp.IsError())
176176
err := opensearch.ParseError(resp)
177177
var testError *opensearch.StringError
178-
require.True(t, errors.As(err, &testError))
178+
require.ErrorAs(t, err, &testError)
179179
assert.Equal(t, http.StatusNotFound, testError.Status)
180180
assert.Contains(t, testError.Err, "{\"_index\":\"index\",\"_id\":\"2\",\"matched\":false}")
181181
_ = fmt.Sprintf("%s", testError)
@@ -193,7 +193,7 @@ func TestError(t *testing.T) {
193193
assert.True(t, resp.IsError())
194194
err := opensearch.ParseError(resp)
195195
var testError *opensearch.Error
196-
require.True(t, errors.As(err, &testError))
196+
require.ErrorAs(t, err, &testError)
197197
assert.Contains(t, testError.Err, "no handler found for uri [/_plugins/_security/xxx] and method [GET]")
198198
_ = fmt.Sprintf("%s", testError)
199199
})
@@ -214,7 +214,7 @@ func TestError(t *testing.T) {
214214
assert.True(t, resp.IsError())
215215
err := opensearch.ParseError(resp)
216216
var testError *opensearch.ReasonError
217-
require.True(t, errors.As(err, &testError))
217+
require.ErrorAs(t, err, &testError)
218218
assert.Equal(t, "error", testError.Status)
219219
assert.Contains(t, testError.Reason, "Invalid configuration")
220220
_ = fmt.Sprintf("%s", testError)
@@ -230,7 +230,7 @@ func TestError(t *testing.T) {
230230
assert.True(t, resp.IsError())
231231
err := opensearch.ParseError(resp)
232232
var testError *opensearch.MessageError
233-
require.True(t, errors.As(err, &testError))
233+
require.ErrorAs(t, err, &testError)
234234
assert.Equal(t, "BAD_REQUEST", testError.Status)
235235
assert.Contains(t, testError.Message, "Wrong request body")
236236
_ = fmt.Sprintf("%s", testError)
@@ -277,7 +277,7 @@ func TestError(t *testing.T) {
277277
t.Run(tt.Name, func(t *testing.T) {
278278
err := opensearch.ParseError(tt.Resp)
279279
for _, wantedError := range tt.WantedErrors {
280-
assert.True(t, errors.Is(err, wantedError))
280+
assert.ErrorIs(t, err, wantedError)
281281
}
282282
})
283283
}
@@ -289,7 +289,7 @@ func TestError(t *testing.T) {
289289
}
290290
assert.True(t, resp.IsError())
291291
err := opensearch.ParseError(resp)
292-
assert.True(t, errors.Is(err, opensearch.ErrJSONUnmarshalBody))
292+
assert.ErrorIs(t, err, opensearch.ErrJSONUnmarshalBody)
293293
})
294294
t.Run("too many requests", func(t *testing.T) {
295295
resp := &opensearch.Response{
@@ -298,7 +298,7 @@ func TestError(t *testing.T) {
298298
}
299299
assert.True(t, resp.IsError())
300300
err := opensearch.ParseError(resp)
301-
assert.True(t, errors.Is(err, opensearch.ErrJSONUnmarshalBody))
301+
assert.ErrorIs(t, err, opensearch.ErrJSONUnmarshalBody)
302302
})
303303
})
304304
}

0 commit comments

Comments
 (0)