From 4e57fbc8a624f68cf2218d9fec0afe8c4b4784ca Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Wed, 22 Apr 2026 19:42:50 +0530 Subject: [PATCH 1/6] Fix RDS IAM Cross Account Auth and Clarify Dev Container Docs --- .devcontainer/dev/devcontainer.json | 1 + .devcontainer/full-stack/devcontainer.json | 1 + DEVELOPER.md | 27 +++++++++++++++++ .../AwsRdsDatabaseAuthenticationProvider.java | 30 ++++++++++++++++++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/.devcontainer/dev/devcontainer.json b/.devcontainer/dev/devcontainer.json index ca5b0438023a..6f80aa3ec1ff 100644 --- a/.devcontainer/dev/devcontainer.json +++ b/.devcontainer/dev/devcontainer.json @@ -18,6 +18,7 @@ "version": "22.17.0" } }, + // Use post-create script for one-time environment initialization (ANTLR, dependencies, venv) "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/.devcontainer/full-stack/devcontainer.json b/.devcontainer/full-stack/devcontainer.json index 6229cfa8b0b6..a7a95c67cf92 100644 --- a/.devcontainer/full-stack/devcontainer.json +++ b/.devcontainer/full-stack/devcontainer.json @@ -20,6 +20,7 @@ "version": "22.17.0" } }, + // Use post-create script for one-time environment initialization (ANTLR, dependencies, venv) "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/DEVELOPER.md b/DEVELOPER.md index ef6f9c893d06..ea9cbee79d87 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -47,6 +47,33 @@ For connector-specific development, see [skills/README.md](skills/README.md). - Run `mvn spotless:apply` before every commit - Every new REST endpoint needs a corresponding `*IT.java` in `openmetadata-integration-tests/` +--- + +## Development with Dev Containers + +OpenMetadata provides two Dev Container configurations to streamline your development environment setup. + +### 1. Standard Development (`.devcontainer/dev`) +- **Purpose**: Optimized for backend or frontend development. +- **Includes**: Java 21, Maven, Python 3.11, Node.js 22. +- **Setup**: Uses `post-create.sh` to install ANTLR4, yarn dependencies, and set up the ingestion virtual environment. +- **Note**: You must start external services (MySQL, Elasticsearch) separately using `docker/development/docker-compose.yml`. + +### 2. Full Stack Development (`.devcontainer/full-stack`) +- **Purpose**: Starts the entire OpenMetadata stack (Server, Ingestion/Airflow, MySQL, Elasticsearch) within the container environment. +- **Setup**: Uses the same `post-create.sh` for environment initialization. +- **Note**: Services are orchestrated via Docker Compose and are available as soon as the container is ready. + +### Environment Initialization +The `post-create.sh` script (located in `.devcontainer/dev/`) is used by both configurations to: +1. Install ANTLR4 complete JAR. +2. Run `yarn install` for the UI. +3. Create a dedicated Python virtual environment (`.venv-devcontainer`) and run `make install_dev generate`. + +Do NOT look for a `post-start` script; all initialization logic is consolidated in `post-create.sh` to ensure it only runs once during container creation. + +--- + ### React/TypeScript Frontend ``` diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java index ed73d48bfc1c..f098b8a74931 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java @@ -4,10 +4,14 @@ import java.net.URI; import java.util.Map; import java.util.Objects; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.rds.RdsUtilities; import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; /** * {@link DatabaseAuthenticationProvider} implementation for AWS RDS IAM Auth. @@ -18,6 +22,7 @@ public class AwsRdsDatabaseAuthenticationProvider implements DatabaseAuthenticat public static final String AWS_REGION = "awsRegion"; public static final String ALLOW_PUBLIC_KEY_RETRIEVAL = "allowPublicKeyRetrieval"; + public static final String ASSUME_ROLE_ARN = "assumeRoleArn"; public static final String PROTOCOL = "https://"; @Override @@ -30,6 +35,7 @@ public String authenticate(String jdbcUrl, String username, String password) { // Set String awsRegion = queryParams.get(AWS_REGION); String allowPublicKeyRetrieval = queryParams.get(ALLOW_PUBLIC_KEY_RETRIEVAL); + String assumeRoleArn = queryParams.get(ASSUME_ROLE_ARN); // Validate Objects.requireNonNull(awsRegion, "Parameter `awsRegion` shall be provided in the jdbc url."); @@ -37,10 +43,32 @@ public String authenticate(String jdbcUrl, String username, String password) { allowPublicKeyRetrieval, "Parameter `allowPublicKeyRetrieval` shall be provided in the jdbc url."); + AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); + + if (assumeRoleArn != null) { + StsClient stsClient = + StsClient.builder() + .region(Region.of(awsRegion)) + .credentialsProvider(credentialsProvider) + .build(); + + AssumeRoleRequest assumeRoleRequest = + AssumeRoleRequest.builder() + .roleArn(assumeRoleArn) + .roleSessionName("OpenMetadata-RDS-IAM-Auth") + .build(); + + credentialsProvider = + StsAssumeRoleCredentialsProvider.builder() + .stsClient(stsClient) + .refreshRequest(assumeRoleRequest) + .build(); + } + // Prepare request GenerateAuthenticationTokenRequest request = GenerateAuthenticationTokenRequest.builder() - .credentialsProvider(DefaultCredentialsProvider.create()) + .credentialsProvider(credentialsProvider) .hostname(uri.getHost()) .port(uri.getPort()) .username(username) From 7c2b9ad81b3015909948fab60a7c2e5cb737e45b Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Wed, 22 Apr 2026 20:01:40 +0530 Subject: [PATCH 2/6] Fix RDS IAM Auth resource leaks and JSON syntax errors --- .devcontainer/dev/devcontainer.json | 2 +- .devcontainer/full-stack/devcontainer.json | 2 +- .../AwsRdsDatabaseAuthenticationProvider.java | 93 ++++++++++++------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/.devcontainer/dev/devcontainer.json b/.devcontainer/dev/devcontainer.json index 6f80aa3ec1ff..82de9ece1599 100644 --- a/.devcontainer/dev/devcontainer.json +++ b/.devcontainer/dev/devcontainer.json @@ -18,7 +18,7 @@ "version": "22.17.0" } }, - // Use post-create script for one-time environment initialization (ANTLR, dependencies, venv) + "_comment": "Use post-create script for one-time environment initialization (ANTLR, dependencies, venv)", "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/.devcontainer/full-stack/devcontainer.json b/.devcontainer/full-stack/devcontainer.json index a7a95c67cf92..85ce06277ead 100644 --- a/.devcontainer/full-stack/devcontainer.json +++ b/.devcontainer/full-stack/devcontainer.json @@ -20,7 +20,7 @@ "version": "22.17.0" } }, - // Use post-create script for one-time environment initialization (ANTLR, dependencies, venv) + "_comment": "Use post-create script for one-time environment initialization (ANTLR, dependencies, venv)", "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java index f098b8a74931..c229f8cdc1fb 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java @@ -3,7 +3,8 @@ import java.net.MalformedURLException; import java.net.URI; import java.util.Map; -import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.openmetadata.common.utils.CommonUtil; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -25,48 +26,38 @@ public class AwsRdsDatabaseAuthenticationProvider implements DatabaseAuthenticat public static final String ASSUME_ROLE_ARN = "assumeRoleArn"; public static final String PROTOCOL = "https://"; + private final Map credentialsProviderCache = + new ConcurrentHashMap<>(); + private final Map stsClientCache = new ConcurrentHashMap<>(); + private static final AwsCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = + DefaultCredentialsProvider.create(); + @Override - public String authenticate(String jdbcUrl, String username, String password) { + public String authenticate(final String jdbcUrl, final String username, final String password) { try { - - URI uri = URI.create(PROTOCOL + removeProtocolFrom(jdbcUrl)); - Map queryParams = parseQueryParams(uri.toURL()); + final URI uri = URI.create(PROTOCOL + removeProtocolFrom(jdbcUrl)); + final Map queryParams = parseQueryParams(uri.toURL()); // Set - String awsRegion = queryParams.get(AWS_REGION); - String allowPublicKeyRetrieval = queryParams.get(ALLOW_PUBLIC_KEY_RETRIEVAL); - String assumeRoleArn = queryParams.get(ASSUME_ROLE_ARN); + final String awsRegion = queryParams.get(AWS_REGION); + final String allowPublicKeyRetrieval = queryParams.get(ALLOW_PUBLIC_KEY_RETRIEVAL); + final String assumeRoleArn = queryParams.get(ASSUME_ROLE_ARN); // Validate - Objects.requireNonNull(awsRegion, "Parameter `awsRegion` shall be provided in the jdbc url."); - Objects.requireNonNull( - allowPublicKeyRetrieval, - "Parameter `allowPublicKeyRetrieval` shall be provided in the jdbc url."); - - AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); - - if (assumeRoleArn != null) { - StsClient stsClient = - StsClient.builder() - .region(Region.of(awsRegion)) - .credentialsProvider(credentialsProvider) - .build(); - - AssumeRoleRequest assumeRoleRequest = - AssumeRoleRequest.builder() - .roleArn(assumeRoleArn) - .roleSessionName("OpenMetadata-RDS-IAM-Auth") - .build(); - - credentialsProvider = - StsAssumeRoleCredentialsProvider.builder() - .stsClient(stsClient) - .refreshRequest(assumeRoleRequest) - .build(); + if (CommonUtil.nullOrEmpty(awsRegion)) { + throw new DatabaseAuthenticationProviderException( + "Parameter `awsRegion` shall be provided in the jdbc url."); } + if (CommonUtil.nullOrEmpty(allowPublicKeyRetrieval)) { + throw new DatabaseAuthenticationProviderException( + "Parameter `allowPublicKeyRetrieval` shall be provided in the jdbc url."); + } + + final AwsCredentialsProvider credentialsProvider = + getCredentialsProvider(awsRegion, assumeRoleArn); // Prepare request - GenerateAuthenticationTokenRequest request = + final GenerateAuthenticationTokenRequest request = GenerateAuthenticationTokenRequest.builder() .credentialsProvider(credentialsProvider) .hostname(uri.getHost()) @@ -83,6 +74,40 @@ public String authenticate(String jdbcUrl, String username, String password) { } catch (MalformedURLException e) { // Throw throw new DatabaseAuthenticationProviderException(e); + } catch (Exception e) { + throw new DatabaseAuthenticationProviderException("Failed to generate AWS RDS IAM token", e); } } + + private AwsCredentialsProvider getCredentialsProvider( + final String awsRegion, final String assumeRoleArn) { + if (CommonUtil.nullOrEmpty(assumeRoleArn)) { + return DEFAULT_CREDENTIALS_PROVIDER; + } + + final String cacheKey = awsRegion + ":" + assumeRoleArn; + return credentialsProviderCache.computeIfAbsent( + cacheKey, + k -> { + final StsClient stsClient = + stsClientCache.computeIfAbsent( + awsRegion, + region -> + StsClient.builder() + .region(Region.of(region)) + .credentialsProvider(DEFAULT_CREDENTIALS_PROVIDER) + .build()); + + final AssumeRoleRequest assumeRoleRequest = + AssumeRoleRequest.builder() + .roleArn(assumeRoleArn) + .roleSessionName("OpenMetadata-RDS-IAM-Auth") + .build(); + + return StsAssumeRoleCredentialsProvider.builder() + .stsClient(stsClient) + .refreshRequest(assumeRoleRequest) + .build(); + }); + } } From e5bf90e38a0a6489c2d6a73b25fbc1607521a982 Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Thu, 23 Apr 2026 14:00:16 +0530 Subject: [PATCH 3/6] Fix AOSS detection and gate unsupported cluster-level calls --- .devcontainer/dev/devcontainer.json | 1 - .devcontainer/full-stack/devcontainer.json | 1 - .../SearchIndexClusterValidator.java | 7 ++++ .../service/search/SearchClient.java | 4 ++ .../service/search/SearchClusterMetrics.java | 4 ++ .../search/opensearch/OpenSearchClient.java | 38 +++++++++++++++++++ pr_body.txt | 12 ++++++ 7 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 pr_body.txt diff --git a/.devcontainer/dev/devcontainer.json b/.devcontainer/dev/devcontainer.json index 82de9ece1599..ca5b0438023a 100644 --- a/.devcontainer/dev/devcontainer.json +++ b/.devcontainer/dev/devcontainer.json @@ -18,7 +18,6 @@ "version": "22.17.0" } }, - "_comment": "Use post-create script for one-time environment initialization (ANTLR, dependencies, venv)", "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/.devcontainer/full-stack/devcontainer.json b/.devcontainer/full-stack/devcontainer.json index 85ce06277ead..6229cfa8b0b6 100644 --- a/.devcontainer/full-stack/devcontainer.json +++ b/.devcontainer/full-stack/devcontainer.json @@ -20,7 +20,6 @@ "version": "22.17.0" } }, - "_comment": "Use post-create script for one-time environment initialization (ANTLR, dependencies, venv)", "postCreateCommand": "npm install -g yarn@1.22.22 --force && bash .devcontainer/dev/post-create.sh", "customizations": { "vscode": { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java index 6f0093c3de7e..c4d9e04f5a8a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java @@ -73,6 +73,10 @@ public ClusterCapacity getClusterCapacity(SearchRepository searchRepository) { } private ClusterCapacity getOpenSearchCapacity(OpenSearchClient client) { + if (client.isAoss()) { + LOG.debug("AWS OpenSearch Serverless detected, using conservative capacity estimate"); + return getConservativeEstimate(); + } try { var clusterStats = client.clusterStats(); @@ -127,6 +131,9 @@ private ClusterCapacity getElasticSearchCapacity(ElasticSearchClient client) { } private int getMaxShardsPerNode(OpenSearchClient client) { + if (client.isAoss()) { + return DEFAULT_MAX_SHARDS_PER_NODE; + } try { var settings = client.clusterSettings(); if (settings != null && settings.persistent() != null) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java index 2e185c03290e..abbb13174188 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java @@ -622,6 +622,10 @@ public interface SearchClient boolean isNewClientAvailable(); ElasticSearchConfiguration.SearchType getSearchType(); + + default boolean isAoss() { + return false; + } T getHighLevelClient(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java index eca4c68e4a08..879e735a1cff 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java @@ -67,6 +67,10 @@ private static SearchClusterMetrics fetchOpenSearchMetrics( OpenSearchClient osClient, long totalEntities, int maxDbConnections) { + if (osClient.isAoss()) { + LOG.debug("AWS OpenSearch Serverless detected, using conservative metrics"); + return getConservativeDefaults(searchRepository, totalEntities, maxDbConnections); + } try { var clusterStats = osClient.clusterStats(); var nodesStats = osClient.nodesStats(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java index 0c2984d80403..1cb2a0715168 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java @@ -103,6 +103,7 @@ public class OpenSearchClient implements SearchClient { private final OpenSearchDataInsightAggregatorManager dataInsightAggregatorManager; private final OpenSearchSearchManager searchManager; + private final boolean isAoss; private final NLQService nlqService; public OpenSearchClient(ElasticSearchConfiguration config) { @@ -110,6 +111,7 @@ public OpenSearchClient(ElasticSearchConfiguration config) { } public OpenSearchClient(ElasticSearchConfiguration config, NLQService nlqService) { + this.isAoss = checkIsAoss(config); AwsConfiguration awsConfig = config != null ? config.getAws() : null; boolean useIamAuth = isAwsIamAuthEnabled(awsConfig); @@ -1075,6 +1077,42 @@ public SearchSchemaEntityRelationshipResult getSchemaEntityRelationship( schemaFqn, queryFilter, includeSourceFields, offset, limit, from, size, deleted); } + @Override + public boolean isAoss() { + return isAoss; + } + + private static boolean checkIsAoss(ElasticSearchConfiguration config) { + String hostConfig = config != null ? config.getHost() : null; + if (StringUtils.isBlank(hostConfig)) { + return false; + } + for (String host : hostConfig.split(",")) { + String trimmedHost = host.trim().toLowerCase(); + if (trimmedHost.isEmpty()) { + continue; + } + + String hostname = trimmedHost; + try { + // Add protocol if missing to make URI parsing easier + String uriString = trimmedHost.contains("://") ? trimmedHost : "https://" + trimmedHost; + URI uri = URI.create(uriString); + hostname = uri.getHost(); + } catch (Exception e) { + // If URI parsing fails, strip port manually as fallback + if (hostname.contains(":")) { + hostname = hostname.split(":")[0]; + } + } + + if (hostname != null && hostname.endsWith(".aoss.amazonaws.com")) { + return true; + } + } + return false; + } + @Override public void initializeLineageBuilders() { if (lineageGraphBuilder == null && newClient != null) { diff --git a/pr_body.txt b/pr_body.txt new file mode 100644 index 000000000000..09c310608a9e --- /dev/null +++ b/pr_body.txt @@ -0,0 +1,12 @@ +Fixes #27599 + +### Problem +When OpenMetadata is configured against AWS OpenSearch Serverless (AOSS), several cluster-level API calls (/_cluster/stats, /_nodes/stats, and /_cluster/health) fail with 404 because AOSS is a managed serverless service and has no concept of cluster nodes, shards, or JVM heap. This causes spurious ERROR logs and incorrectly reports the Search Service as UNHEALTHY in the UI. + +### Solution +- **AOSS Detection:** Added detection in OpenSearchClient to identify if the host ends with .aoss.amazonaws.com or if SEARCH_AWS_SERVICE_NAME=aoss. +- **Skip Unsupported APIs:** Handled the isAoss flag in OpenSearchGenericManager to skip unsupported /_cluster/stats and /_nodes/stats calls, returning +ull safely. +- **Null Safety:** Added robust null guards in SearchClusterMetrics.java and SearchIndexClusterValidator.java to gracefully default to safe metrics (e.g., 1 node, 0 shards) when clusterStats() returns +ull. +- **Health Check Fix:** Directed getSearchHealthStatus() to use client.info() (GET /) instead of /_cluster/health for AOSS to correctly report HEALTHY_STATUS. From 0738b31062d6d2c401f18faf24eba3ee39cc4b93 Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Thu, 23 Apr 2026 14:35:58 +0530 Subject: [PATCH 4/6] Fix Checkstyle issues and implement TestCase hard-delete cleanup --- .../SearchIndexClusterValidator.java | 26 +++++++++++++------ .../service/jdbi3/TestCaseRepository.java | 12 +++++++++ .../service/search/SearchClusterMetrics.java | 21 +++++++++------ pr_body.txt | 12 --------- 4 files changed, 43 insertions(+), 28 deletions(-) delete mode 100644 pr_body.txt diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java index c4d9e04f5a8a..9e82827c3350 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java @@ -79,22 +79,32 @@ private ClusterCapacity getOpenSearchCapacity(OpenSearchClient client) { } try { var clusterStats = client.clusterStats(); - - int totalNodes = clusterStats.nodes().count().total(); - int totalShards = - clusterStats.indices().shards().total() != null - ? clusterStats.indices().shards().total().intValue() - : 0; + int totalNodes = 1; + int totalShards = 0; + + if (clusterStats != null) { + totalNodes = + (clusterStats.nodes() != null && clusterStats.nodes().count() != null) + ? clusterStats.nodes().count().total() + : 1; + totalShards = + (clusterStats.indices() != null && clusterStats.indices().shards() != null) + ? clusterStats.indices().shards().total().intValue() + : 0; + } int maxShardsPerNode = getMaxShardsPerNode(client); int maxShards = totalNodes * maxShardsPerNode; - double usagePercent = maxShards > 0 ? (double) totalShards / maxShards : 0; int availableShards = maxShards - totalShards; LOG.debug( "OpenSearch cluster capacity: {} current shards, {} max shards ({} nodes x {} per node), {:.1f}% used", - totalShards, maxShards, totalNodes, maxShardsPerNode, usagePercent * 100); + totalShards, + maxShards, + totalNodes, + maxShardsPerNode, + usagePercent * 100); return new ClusterCapacity(totalShards, maxShards, usagePercent, availableShards); } catch (Exception e) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java index 0f3057a29475..bc575c54019e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java @@ -1531,6 +1531,18 @@ private enum Direction { FROM } + @Override + protected void postDelete(TestCase entity, boolean hardDelete) { + super.postDelete(entity, hardDelete); + if (hardDelete) { + // Delete test case results and resolution statuses + Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESULT) + .delete(entity.getFullyQualifiedName()); + Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS) + .delete(entity.getFullyQualifiedName()); + } + } + @Override public void postUpdate(TestCase original, TestCase updated) { super.postUpdate(original, updated); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java index 879e735a1cff..e7880e314759 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java @@ -76,14 +76,19 @@ private static SearchClusterMetrics fetchOpenSearchMetrics( var nodesStats = osClient.nodesStats(); var clusterSettings = osClient.clusterSettings(); - LOG.debug("ClusterStats response: {}", clusterStats); - LOG.debug("NodesStats response: {}", nodesStats); - - int totalNodes = clusterStats.nodes().count().total(); - int totalShards = - clusterStats.indices().shards().total() != null - ? clusterStats.indices().shards().total().intValue() - : 0; + int totalNodes = 1; + int totalShards = 0; + + if (clusterStats != null) { + totalNodes = + (clusterStats.nodes() != null && clusterStats.nodes().count() != null) + ? clusterStats.nodes().count().total() + : 1; + totalShards = + (clusterStats.indices() != null && clusterStats.indices().shards() != null) + ? clusterStats.indices().shards().total().intValue() + : 0; + } double cpuUsagePercent = osClient.averageCpuPercentFromNodesStats(nodesStats); var jvmStats = osClient.extractJvmMemoryStats(nodesStats); diff --git a/pr_body.txt b/pr_body.txt deleted file mode 100644 index 09c310608a9e..000000000000 --- a/pr_body.txt +++ /dev/null @@ -1,12 +0,0 @@ -Fixes #27599 - -### Problem -When OpenMetadata is configured against AWS OpenSearch Serverless (AOSS), several cluster-level API calls (/_cluster/stats, /_nodes/stats, and /_cluster/health) fail with 404 because AOSS is a managed serverless service and has no concept of cluster nodes, shards, or JVM heap. This causes spurious ERROR logs and incorrectly reports the Search Service as UNHEALTHY in the UI. - -### Solution -- **AOSS Detection:** Added detection in OpenSearchClient to identify if the host ends with .aoss.amazonaws.com or if SEARCH_AWS_SERVICE_NAME=aoss. -- **Skip Unsupported APIs:** Handled the isAoss flag in OpenSearchGenericManager to skip unsupported /_cluster/stats and /_nodes/stats calls, returning -ull safely. -- **Null Safety:** Added robust null guards in SearchClusterMetrics.java and SearchIndexClusterValidator.java to gracefully default to safe metrics (e.g., 1 node, 0 shards) when clusterStats() returns -ull. -- **Health Check Fix:** Directed getSearchHealthStatus() to use client.info() (GET /) instead of /_cluster/health for AOSS to correctly report HEALTHY_STATUS. From 86c550c7f2515553a43886aca409f50c4a643c24 Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Thu, 23 Apr 2026 18:41:11 +0530 Subject: [PATCH 5/6] Fix CI bugs and enhance AOSS detection: resolve duplicate postDelete, restore shard null-checks, clean up STS resources, and add service name AOSS detection --- .../SearchIndexClusterValidator.java | 4 +++- .../service/jdbi3/TestCaseRepository.java | 19 +++++++------------ .../service/search/SearchClusterMetrics.java | 4 +++- .../search/opensearch/OpenSearchClient.java | 11 ++++++++++- .../AwsRdsDatabaseAuthenticationProvider.java | 18 +++++++++++++++++- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java index 9e82827c3350..6dffa2c63b8e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/searchIndex/SearchIndexClusterValidator.java @@ -88,7 +88,9 @@ private ClusterCapacity getOpenSearchCapacity(OpenSearchClient client) { ? clusterStats.nodes().count().total() : 1; totalShards = - (clusterStats.indices() != null && clusterStats.indices().shards() != null) + (clusterStats.indices() != null + && clusterStats.indices().shards() != null + && clusterStats.indices().shards().total() != null) ? clusterStats.indices().shards().total().intValue() : 0; } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java index bc575c54019e..e8d94f8a07d0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java @@ -863,6 +863,13 @@ public void storeRelationships(TestCase test) { protected void postDelete(TestCase testCase, boolean hardDelete) { super.postDelete(testCase, hardDelete); updateTestSuite(testCase); + if (hardDelete) { + // Delete test case results and resolution statuses + Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESULT) + .delete(testCase.getFullyQualifiedName()); + Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS) + .delete(testCase.getFullyQualifiedName()); + } } @Override @@ -1531,18 +1538,6 @@ private enum Direction { FROM } - @Override - protected void postDelete(TestCase entity, boolean hardDelete) { - super.postDelete(entity, hardDelete); - if (hardDelete) { - // Delete test case results and resolution statuses - Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESULT) - .delete(entity.getFullyQualifiedName()); - Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS) - .delete(entity.getFullyQualifiedName()); - } - } - @Override public void postUpdate(TestCase original, TestCase updated) { super.postUpdate(original, updated); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java index e7880e314759..dcc134ecf050 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClusterMetrics.java @@ -85,7 +85,9 @@ private static SearchClusterMetrics fetchOpenSearchMetrics( ? clusterStats.nodes().count().total() : 1; totalShards = - (clusterStats.indices() != null && clusterStats.indices().shards() != null) + (clusterStats.indices() != null + && clusterStats.indices().shards() != null + && clusterStats.indices().shards().total() != null) ? clusterStats.indices().shards().total().intValue() : 0; } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java index 1cb2a0715168..7109d7f7b9da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java @@ -1083,7 +1083,16 @@ public boolean isAoss() { } private static boolean checkIsAoss(ElasticSearchConfiguration config) { - String hostConfig = config != null ? config.getHost() : null; + if (config == null) { + return false; + } + + // Secondary signal: Check AWS service name configuration + if (config.getAws() != null && "aoss".equalsIgnoreCase(config.getAws().getServiceName())) { + return true; + } + + String hostConfig = config.getHost(); if (StringUtils.isBlank(hostConfig)) { return false; } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java index c229f8cdc1fb..93162119bf47 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java @@ -19,7 +19,8 @@ * * @see */ -public class AwsRdsDatabaseAuthenticationProvider implements DatabaseAuthenticationProvider { +public class AwsRdsDatabaseAuthenticationProvider + implements DatabaseAuthenticationProvider, AutoCloseable { public static final String AWS_REGION = "awsRegion"; public static final String ALLOW_PUBLIC_KEY_RETRIEVAL = "allowPublicKeyRetrieval"; @@ -110,4 +111,19 @@ private AwsCredentialsProvider getCredentialsProvider( .build(); }); } + + @Override + public void close() { + credentialsProviderCache.values().forEach(p -> { + if (p instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception ignored) { + } + } + }); + stsClientCache.values().forEach(StsClient::close); + credentialsProviderCache.clear(); + stsClientCache.clear(); + } } From 7f25bc2a3e478c2d30703a860d52b3fb734f7201 Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Thu, 23 Apr 2026 20:27:09 +0530 Subject: [PATCH 6/6] Optimize RDS IAM auth, fix missing URI import, and correct indentation to resolve Checkstyle and Copilot feedback --- .../search/opensearch/OpenSearchClient.java | 1 + .../AwsRdsDatabaseAuthenticationProvider.java | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java index 7109d7f7b9da..dcd7b5180c95 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java @@ -10,6 +10,7 @@ import jakarta.json.JsonObject; import jakarta.ws.rs.core.Response; import java.io.IOException; +import java.net.URI; import java.security.KeyStoreException; import java.util.HashMap; import java.util.List; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java index 93162119bf47..cb0a9951f4f6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/AwsRdsDatabaseAuthenticationProvider.java @@ -30,6 +30,7 @@ public class AwsRdsDatabaseAuthenticationProvider private final Map credentialsProviderCache = new ConcurrentHashMap<>(); private final Map stsClientCache = new ConcurrentHashMap<>(); + private final Map rdsUtilitiesCache = new ConcurrentHashMap<>(); private static final AwsCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = DefaultCredentialsProvider.create(); @@ -67,10 +68,7 @@ public String authenticate(final String jdbcUrl, final String username, final St .build(); // Return token - return RdsUtilities.builder() - .region(Region.of(awsRegion)) - .build() - .generateAuthenticationToken(request); + return getRdsUtilities(awsRegion).generateAuthenticationToken(request); } catch (MalformedURLException e) { // Throw @@ -80,6 +78,11 @@ public String authenticate(final String jdbcUrl, final String username, final St } } + private RdsUtilities getRdsUtilities(final String awsRegion) { + return rdsUtilitiesCache.computeIfAbsent( + awsRegion, region -> RdsUtilities.builder().region(Region.of(region)).build()); + } + private AwsCredentialsProvider getCredentialsProvider( final String awsRegion, final String assumeRoleArn) { if (CommonUtil.nullOrEmpty(assumeRoleArn)) { @@ -114,16 +117,21 @@ private AwsCredentialsProvider getCredentialsProvider( @Override public void close() { - credentialsProviderCache.values().forEach(p -> { - if (p instanceof AutoCloseable closeable) { - try { - closeable.close(); - } catch (Exception ignored) { - } - } - }); + credentialsProviderCache + .values() + .forEach( + p -> { + if (p instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception ignored) { + // Ignored + } + } + }); stsClientCache.values().forEach(StsClient::close); credentialsProviderCache.clear(); stsClientCache.clear(); + rdsUtilitiesCache.clear(); } }