This is an automated email from the ASF dual-hosted git repository.

smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 8ca44914771 HDDS-15087. S3 Gateway and Local S3 UX (#10759)
8ca44914771 is described below

commit 8ca449147710bb69be1b77b22d24d0f70709f72c
Author: Chun-Hung Tseng <[email protected]>
AuthorDate: Fri Oct 9 06:46:43 2026 +0200

    HDDS-15087. S3 Gateway and Local S3 UX (#10759)
---
 .../common/src/main/resources/ozone-default.xml    |  11 ++
 hadoop-hdds/docs/content/tools/_index.md           |   4 +-
 .../hadoop/hdds/server/http/HttpServer2.java       |   3 +
 hadoop-ozone/integration-test-s3/pom.xml           |   5 +
 .../hadoop/ozone/local/TestLocalOzoneS3.java       |  95 ++++++++++++++
 .../java/org/apache/hadoop/ozone/s3/Gateway.java   |   1 -
 .../hadoop/ozone/s3/OzoneConfigurationHolder.java  |   3 -
 .../hadoop/ozone/s3/S3GatewayConfigKeys.java       |   8 ++
 .../hadoop/ozone/s3/endpoint/EndpointBase.java     |   7 +-
 .../org/apache/hadoop/ozone/s3/util/S3Utils.java   |   8 +-
 .../apache/hadoop/ozone/s3/util/TestS3Utils.java   |  16 ++-
 hadoop-ozone/tools/pom.xml                         |   4 +
 .../hadoop/ozone/local/LocalOzoneCluster.java      | 137 ++++++++++++++++++---
 .../ozone/local/LocalOzoneClusterConfig.java       |  59 ++-------
 .../hadoop/ozone/local/LocalOzoneRuntime.java      |   4 +-
 .../org/apache/hadoop/ozone/local/OzoneLocal.java  |  46 ++-----
 .../hadoop/ozone/local/TestLocalOzoneCluster.java  | 111 +++++++++++++++++
 .../ozone/local/TestLocalOzoneClusterConfig.java   |  11 +-
 .../apache/hadoop/ozone/local/TestOzoneLocal.java  |  76 +++++++-----
 19 files changed, 451 insertions(+), 158 deletions(-)

diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml 
b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index e4be1210fae..4ba267b677c 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -2101,6 +2101,17 @@
     </description>
   </property>
 
+  <property>
+    <name>ozone.s3g.standard.storage-class.use-client-default</name>
+    <value>false</value>
+    <tag>OZONE, S3GATEWAY</tag>
+    <description>
+      When true, a request with x-amz-storage-class STANDARD uses 
ozone.replication and
+      ozone.replication.type, if ozone.replication is set, instead of 
RATIS/THREE.
+      The ozone local runtime sets it to true.
+    </description>
+  </property>
+
   <property>
     <name>ozone.s3g.secret.http.enabled</name>
     <value>false</value>
diff --git a/hadoop-hdds/docs/content/tools/_index.md 
b/hadoop-hdds/docs/content/tools/_index.md
index 3a8bf08e826..adbd4740f87 100644
--- a/hadoop-hdds/docs/content/tools/_index.md
+++ b/hadoop-hdds/docs/content/tools/_index.md
@@ -43,8 +43,8 @@ Client commands:
 
    * **sh** -  Primary command line interface for ozone to manage 
volumes/buckets/keys.
    * **fs** - Runs a command on ozone file system (similar to `hdfs dfs`)
-   * **local** - Runs a single-node local Ozone cluster (SCM, OM, and 
datanodes)
-   in one process for development.
+   * **local** - Runs a single-node local Ozone cluster (SCM, OM, datanodes,
+   and optional S3 Gateway) in one process for development.
    * **version** - Prints the version of Ozone and HDDS.
 
 
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
index 775e1bc1cf9..1568d26fcfe 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
@@ -785,6 +785,9 @@ private static WebAppContext createWebAppContext(Builder b,
       AccessControlList adminsAcl, final String appDir) {
     WebAppContext ctx = new WebAppContext();
     ctx.setDefaultsDescriptor(null);
+    // S3 Gateway starts Weld through its web.xml listener; other services do 
not use CDI.
+    ctx.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern",
+        "org.jboss.weld.environment.servlet.EnhancedListener");
     ServletHolder holder = new ServletHolder(new DefaultServlet());
     Map<String, String> params = ImmutableMap.<String, String>builder()
         .put("acceptRanges", "true")
diff --git a/hadoop-ozone/integration-test-s3/pom.xml 
b/hadoop-ozone/integration-test-s3/pom.xml
index 1f7c884d6f7..9629f2ba31c 100644
--- a/hadoop-ozone/integration-test-s3/pom.xml
+++ b/hadoop-ozone/integration-test-s3/pom.xml
@@ -149,6 +149,11 @@
       <artifactId>ozone-s3gateway</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.ozone</groupId>
+      <artifactId>ozone-tools</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.ratis</groupId>
       <artifactId>ratis-common</artifactId>
diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneS3.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneS3.java
new file mode 100644
index 00000000000..4cdaa7137f9
--- /dev/null
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneS3.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.local;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.UUID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.conf.StorageUnit;
+import org.apache.hadoop.ozone.ClientConfigForTesting;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.StorageClass;
+
+/**
+ * Integration tests for the S3 Gateway of the {@code ozone local} runtime.
+ */
+class TestLocalOzoneS3 {
+
+  @TempDir
+  private Path tempDir;
+
+  /**
+   * Security is off, so the S3 Gateway accepts credentials it has never seen. 
The object carries an explicit STANDARD
+   * storage class, which succeeds on the single default datanode only because 
ozone local sets
+   * {@code ozone.s3g.standard.storage-class.use-client-default}; otherwise 
S3StorageType maps it to RATIS/THREE.
+   */
+  @Test
+  void s3GatewayServesRequests() throws Exception {
+    LocalOzoneClusterConfig config = 
LocalOzoneClusterConfig.builder(tempDir.resolve("local-ozone-s3")).build();
+    OzoneConfiguration conf = new OzoneConfiguration();
+    // Match the test module's 128 MB containers; production blocks default to 
256 MB.
+    ClientConfigForTesting.newBuilder(StorageUnit.MB).applyTo(conf);
+
+    try (LocalOzoneCluster cluster = new LocalOzoneCluster(config, conf)) {
+      cluster.start();
+
+      assertTrue(cluster.getS3gPort() > 0);
+      assertTrue(cluster.getS3gBoundAddress().getAddress().isLoopbackAddress(),
+          () -> cluster.getS3gBoundAddress().toString());
+
+      assertBucketRoundTrip(cluster.getS3Endpoint(), "local-smoke",
+          LocalOzoneClusterConfig.LOCAL_S3_ACCESS_KEY, 
LocalOzoneClusterConfig.LOCAL_S3_SECRET_KEY);
+      String unknown = UUID.randomUUID().toString().replace("-", "");
+      assertBucketRoundTrip(cluster.getS3Endpoint(), "local-smoke-" + unknown, 
unknown, unknown);
+
+      try (S3Client s3 = s3Client(cluster.getS3Endpoint(),
+          LocalOzoneClusterConfig.LOCAL_S3_ACCESS_KEY, 
LocalOzoneClusterConfig.LOCAL_S3_SECRET_KEY)) {
+        s3.putObject(request -> 
request.bucket("local-smoke").key("object").storageClass(StorageClass.STANDARD),
+            RequestBody.fromString("payload"));
+        assertEquals("payload",
+            s3.getObjectAsBytes(request -> 
request.bucket("local-smoke").key("object")).asUtf8String());
+      }
+    }
+  }
+
+  private static void assertBucketRoundTrip(String endpoint, String bucket, 
String accessKey, String secretKey) {
+    try (S3Client s3 = s3Client(endpoint, accessKey, secretKey)) {
+      s3.createBucket(request -> request.bucket(bucket));
+      assertTrue(s3.listBuckets().buckets().stream().anyMatch(b -> 
bucket.equals(b.name())));
+    }
+  }
+
+  private static S3Client s3Client(String endpoint, String accessKey, String 
secretKey) {
+    return S3Client.builder()
+        .endpointOverride(URI.create(endpoint))
+        .region(Region.of(LocalOzoneClusterConfig.LOCAL_S3_REGION))
+        
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey,
 secretKey)))
+        .forcePathStyle(true)
+        .build();
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java
index 41e89fbde35..0b738d01eb8 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java
@@ -160,7 +160,6 @@ private static void loginS3GUser(OzoneConfiguration conf)
     }
   }
 
-  @VisibleForTesting
   public InetSocketAddress getHttpAddress() {
     return this.httpServer.getHttpAddress();
   }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/OzoneConfigurationHolder.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/OzoneConfigurationHolder.java
index 88652f0457a..9a35d293913 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/OzoneConfigurationHolder.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/OzoneConfigurationHolder.java
@@ -17,7 +17,6 @@
 
 package org.apache.hadoop.ozone.s3;
 
-import com.google.common.annotations.VisibleForTesting;
 import javax.enterprise.inject.Produces;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 
@@ -39,7 +38,6 @@ public static OzoneConfiguration configuration() {
     return configuration;
   }
 
-  @VisibleForTesting
   public static void setConfiguration(
       OzoneConfiguration conf) {
     // Nullity check is used in case the configuration was already set
@@ -49,7 +47,6 @@ public static void setConfiguration(
     }
   }
 
-  @VisibleForTesting
   public static void resetConfiguration() {
     configuration = null;
   }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java
index cc674f4b0d6..468dd018099 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java
@@ -102,6 +102,14 @@ public final class S3GatewayConfigKeys {
   public static final String OZONE_S3G_LIST_MAX_KEYS_LIMIT = 
"ozone.s3g.list.max.keys.limit";
   public static final int OZONE_S3G_LIST_MAX_KEYS_LIMIT_DEFAULT = 1000;
 
+  /**
+   * Configuration key that makes an explicit {@code x-amz-storage-class: 
STANDARD} use the {@code ozone.replication}
+   * setting, when set, instead of {@link 
org.apache.hadoop.ozone.s3.util.S3StorageType#STANDARD}.
+   */
+  public static final String 
OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY =
+      "ozone.s3g.standard.storage-class.use-client-default";
+  public static final boolean 
OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_DEFAULT = false;
+
   /**
    * Never constructed.
    */
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index b7b02b5fc53..74a90f1fc6e 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -32,6 +32,8 @@
 import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_CLIENT_BUFFER_SIZE_KEY;
 import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_FSO_DIRECTORY_CREATION_ENABLED;
 import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_FSO_DIRECTORY_CREATION_ENABLED_DEFAULT;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_DEFAULT;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY;
 import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.BUCKET_ALREADY_EXISTS;
 import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.BUCKET_ALREADY_OWNED_BY_YOU;
 import static 
org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT;
@@ -820,9 +822,12 @@ protected ReplicationConfig 
getReplicationConfig(OzoneBucket ozoneBucket) throws
 
     ReplicationConfig clientConfiguredReplicationConfig =
         
OzoneClientUtils.getClientConfiguredReplicationConfig(getOzoneConfiguration());
+    boolean standardUsesClientDefault = getOzoneConfiguration().getBoolean(
+        OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY,
+        OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_DEFAULT);
 
     return S3Utils.resolveS3ClientSideReplicationConfig(storageType, 
storageConfig,
-        clientConfiguredReplicationConfig, ozoneBucket.getReplicationConfig());
+        clientConfiguredReplicationConfig, ozoneBucket.getReplicationConfig(), 
standardUsesClientDefault);
   }
 
   /**
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
index b72c4adb2d2..6acd1ef9e0f 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
@@ -105,17 +105,23 @@ private S3Utils() {
    *                                   config.
    * @param clientConfiguredReplConfig - Client side configured replication
    *                                   config.
+   * @param standardUsesClientDefault  - whether STANDARD resolves to a 
non-null
+   *                                   clientConfiguredReplConfig.
    * @return client resolved replication config.
    */
   public static ReplicationConfig resolveS3ClientSideReplicationConfig(
       String s3StorageTypeHeader, String s3StorageConfigHeader,
       ReplicationConfig clientConfiguredReplConfig,
-      ReplicationConfig bucketReplConfig)
+      ReplicationConfig bucketReplConfig, boolean standardUsesClientDefault)
       throws OS3Exception {
 
     // If user provided s3 storage type header is not null then map it
     // to ozone replication config
     if (!StringUtils.isEmpty(s3StorageTypeHeader)) {
+      if (standardUsesClientDefault && clientConfiguredReplConfig != null
+          && S3StorageType.STANDARD.name().equals(s3StorageTypeHeader)) {
+        return clientConfiguredReplConfig;
+      }
       return toReplicationConfig(s3StorageTypeHeader, s3StorageConfigHeader);
     }
 
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3Utils.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3Utils.java
index f178c507ac9..fe086d57020 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3Utils.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3Utils.java
@@ -82,7 +82,10 @@ public static List<Arguments> validS3ReplicationConfigs() {
       for (String s3StorageConfig : S3STORAGECONFIG) {
         for (ReplicationConfig clientReplConfig : REPLICATIONS) {
           for (ReplicationConfig bucketReplConfig: REPLICATIONS) {
-            args.add(Arguments.of(s3StorageType, s3StorageConfig, 
clientReplConfig, bucketReplConfig));
+            for (boolean standardUsesClientDefault : new boolean[] {false, 
true}) {
+              args.add(Arguments.of(s3StorageType, s3StorageConfig, 
clientReplConfig, bucketReplConfig,
+                  standardUsesClientDefault));
+            }
           }
         }
       }
@@ -93,11 +96,11 @@ public static List<Arguments> validS3ReplicationConfigs() {
   @ParameterizedTest
   @MethodSource("validS3ReplicationConfigs")
   public void testValidResolveS3ClientSideReplicationConfig(String 
s3StorageType, String s3StorageConfig,
-      ReplicationConfig clientConfiguredReplConfig, ReplicationConfig 
bucketReplConfig)
-      throws OS3Exception {
+      ReplicationConfig clientConfiguredReplConfig, ReplicationConfig 
bucketReplConfig,
+      boolean standardUsesClientDefault) throws OS3Exception {
     ReplicationConfig replicationConfig = S3Utils
         .resolveS3ClientSideReplicationConfig(s3StorageType, s3StorageConfig,
-            clientConfiguredReplConfig, bucketReplConfig);
+            clientConfiguredReplConfig, bucketReplConfig, 
standardUsesClientDefault);
 
     final ReplicationConfig expectedReplConfig;
     if (!StringUtils.isEmpty(s3StorageType)) {
@@ -108,7 +111,8 @@ public void 
testValidResolveS3ClientSideReplicationConfig(String s3StorageType,
           expectedReplConfig = EC32REPLICATIONCONFIG;
         }
       } else if (S3StorageType.STANDARD.name().equals(s3StorageType)) {
-        expectedReplConfig = RATIS3REPLICATIONCONFIG;
+        expectedReplConfig = standardUsesClientDefault && 
clientConfiguredReplConfig != null
+            ? clientConfiguredReplConfig : RATIS3REPLICATIONCONFIG;
       } else {
         expectedReplConfig = RATIS1REPLICATIONCONFIG;
       }
@@ -146,7 +150,7 @@ public void 
testResolveRepConfWhenUserPassedIsInvalid(String s3StorageType, Stri
       throws OS3Exception {
     OS3Exception exception = assertThrows(OS3Exception.class, () -> S3Utils.
         resolveS3ClientSideReplicationConfig(
-            s3StorageType, s3StorageConfig, clientConfiguredReplConfig, 
bucketReplConfig));
+            s3StorageType, s3StorageConfig, clientConfiguredReplConfig, 
bucketReplConfig, false));
     assertEquals(S3ErrorTable.INVALID_STORAGE_CLASS.getCode(), 
exception.getCode());
   }
 
diff --git a/hadoop-ozone/tools/pom.xml b/hadoop-ozone/tools/pom.xml
index 8429437e5ad..bdd362d9ba4 100644
--- a/hadoop-ozone/tools/pom.xml
+++ b/hadoop-ozone/tools/pom.xml
@@ -87,6 +87,10 @@
       <groupId>org.apache.ozone</groupId>
       <artifactId>ozone-manager</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.ozone</groupId>
+      <artifactId>ozone-s3gateway</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.ratis</groupId>
       <artifactId>ratis-common</artifactId>
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
index fdab4179bd8..8bb057f601b 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
@@ -54,6 +54,8 @@
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.HDDS_CONTAINER_RATIS_IPC_PORT;
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.HDDS_CONTAINER_RATIS_SERVER_PORT;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_HTTP_BASEDIR;
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_HTTP_POLICY_DEFAULT;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_HTTP_POLICY_KEY;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_METADATA_DIRS;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION_TYPE;
@@ -72,10 +74,21 @@
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SNAPSHOT_DIFF_DB_DIR;
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_BIND_HOST_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_BIND_HOST_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ENABLED_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTPS_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTPS_BIND_HOST_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTP_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTP_BIND_HOST_KEY;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -103,6 +116,7 @@
 import org.apache.hadoop.hdds.scm.proxy.SCMClientConfig;
 import org.apache.hadoop.hdds.scm.server.SCMStorageConfig;
 import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.hdds.server.http.HttpConfig;
 import org.apache.hadoop.hdds.utils.IOUtils;
 import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
 import org.apache.hadoop.ozone.HddsDatanodeService;
@@ -111,13 +125,14 @@
 import org.apache.hadoop.ozone.container.replication.ReplicationServer;
 import org.apache.hadoop.ozone.om.OMStorage;
 import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.s3.Gateway;
+import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
+import org.apache.ratis.util.function.CheckedSupplier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Starts the SCM, OM, and datanode portion of the {@code ozone local} runtime.
- *
- * <p>S3 Gateway and Recon are added by later local runtime tickets.</p>
+ * Starts the SCM, OM, datanode, and optional S3 Gateway portion of the {@code 
ozone local} runtime.
  */
 public final class LocalOzoneCluster implements LocalOzoneRuntime {
 
@@ -145,6 +160,8 @@ public final class LocalOzoneCluster implements 
LocalOzoneRuntime {
   private static final String OM_HTTP_PORT_KEY = "om.http";
   private static final String OM_HTTPS_PORT_KEY = "om.https";
   private static final String OM_RATIS_PORT_KEY = "om.ratis";
+  private static final String S3G_HTTP_PORT_KEY = "s3g.http";
+  private static final String S3G_WEBADMIN_HTTP_PORT_KEY = "s3g.web.http";
   private static final String DATANODE_PORT_KEY_PREFIX = "dn.";
   private static final String DATANODE_HTTP_PORT_KEY_SUFFIX = "http";
   private static final String DATANODE_CLIENT_PORT_KEY_SUFFIX = "client";
@@ -205,6 +222,7 @@ public final class LocalOzoneCluster implements 
LocalOzoneRuntime {
   private PreparedConfiguration preparedConfiguration;
   private StorageContainerManager scm;
   private OzoneManager om;
+  private Gateway s3Gateway;
   private final List<HddsDatanodeService> datanodes = new ArrayList<>();
   private boolean previousMetricsMiniClusterMode;
   private boolean metricsMiniClusterModeEnabled;
@@ -236,6 +254,9 @@ public void start() throws Exception {
       startOm(prepared.getConfiguration());
       startDatanodes(prepared.getDatanodeConfigurations());
       waitForClusterReadiness(config.getStartupTimeout());
+      if (config.isS3gEnabled()) {
+        startS3Gateway(prepared.getConfiguration());
+      }
     } catch (Exception ex) {
       // Roll back without latching closed: the caller's close() still owns the
       // ephemeral data dir lifecycle.
@@ -257,23 +278,23 @@ PreparedConfiguration prepareConfiguration() throws 
IOException {
     configureLocalDefaults(conf);
 
     PersistedPortState persistedPorts = loadPersistedPortState();
-    PortAllocator portAllocator = new PortAllocator();
+    PortAllocator portAllocator = new PortAllocator(persistedPorts.ports());
     int scmPort = configureScm(conf, persistedPorts, portAllocator);
     int omPort = configureOm(conf, persistedPorts, portAllocator);
+    int s3gPort = configureS3Gateway(conf, persistedPorts, portAllocator);
     List<OzoneConfiguration> datanodeConfigurations =
         configureDatanodes(conf, persistedPorts, portAllocator);
 
     prepareStorageLayout();
     createServiceDirectories();
     persistedPorts.store();
-    preparedConfiguration = new PreparedConfiguration(conf, scmPort, omPort,
-        datanodeConfigurations);
+    preparedConfiguration = new PreparedConfiguration(conf, scmPort, omPort, 
s3gPort, datanodeConfigurations);
     return preparedConfiguration;
   }
 
   @Override
   public String getDisplayHost() {
-    return LocalOzoneClusterConfig.DEFAULT_BIND_HOST.equals(config.getHost())
+    return LocalOzoneClusterConfig.WILDCARD_HOST.equals(config.getHost())
         ? LocalOzoneClusterConfig.DEFAULT_HOST : config.getHost();
   }
 
@@ -296,12 +317,19 @@ public int getDatanodeCount() {
 
   @Override
   public int getS3gPort() {
-    return -1;
+    InetSocketAddress address = getS3gBoundAddress();
+    return address != null ? address.getPort() : -1;
   }
 
   @Override
   public String getS3Endpoint() {
-    return "";
+    int port = getS3gPort();
+    return port > 0 ? "http://"; + address(getDisplayHost(), port) : "";
+  }
+
+  /** Returns the bound S3 Gateway HTTP address, or null when the S3 Gateway 
is not running. */
+  InetSocketAddress getS3gBoundAddress() {
+    return s3Gateway != null ? s3Gateway.getHttpAddress() : null;
   }
 
   @Override
@@ -330,12 +358,20 @@ private void stopServices() {
     try {
       // Logged rather than closeQuietly: stopServices() is also the start() 
rollback, where a silent
       // failure hides leaked threads and ports.
-      IOUtils.close(LOG, this::stopDatanodes, this::stopOm, this::stopScm);
+      IOUtils.close(LOG, this::stopS3Gateway, this::stopDatanodes, 
this::stopOm, this::stopScm);
     } finally {
       restoreSameJvmMetricsMode();
     }
   }
 
+  private void stopS3Gateway() throws Exception {
+    Gateway service = s3Gateway;
+    s3Gateway = null;
+    if (service != null) {
+      service.stop();
+    }
+  }
+
   private void stopDatanodes() {
     List<AutoCloseable> stoppers = new ArrayList<>();
     for (int i = datanodes.size() - 1; i >= 0; i--) {
@@ -610,6 +646,37 @@ private void configureOmStorage(OzoneConfiguration conf) {
         omMetadataDir.toString());
   }
 
+  private int configureS3Gateway(OzoneConfiguration conf, PersistedPortState 
persistedPorts,
+      PortAllocator portAllocator) throws IOException {
+    if (!config.isS3gEnabled()) {
+      return -1;
+    }
+    // getS3Endpoint() advertises the HTTP listener, which a policy without 
HTTP leaves unbound.
+    HttpConfig.Policy httpPolicy =
+        HttpConfig.Policy.fromString(conf.get(OZONE_HTTP_POLICY_KEY, 
OZONE_HTTP_POLICY_DEFAULT));
+    if (httpPolicy != null && !httpPolicy.isHttpEnabled()) {
+      rejectUserConfigured(conf, OZONE_HTTP_POLICY_KEY, "HTTP_ONLY or 
HTTP_AND_HTTPS");
+    }
+    // getS3gPort() reads the port back off this listener.
+    setLocalOverride(conf, OZONE_S3G_HTTP_ENABLED_KEY, true);
+    // S3StorageType maps an explicit STANDARD to RATIS/THREE regardless of 
the local STAND_ALONE/ONE replication.
+    setLocalOverride(conf, 
OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY, true);
+
+    int s3gHttpPort = reservePort(portAllocator, persistedPorts, 
S3G_HTTP_PORT_KEY, config.getS3gPort());
+    int s3gWebHttpPort = reservePort(portAllocator, persistedPorts, 
S3G_WEBADMIN_HTTP_PORT_KEY, 0);
+
+    // HTTPS addresses stay on port 0: the printed endpoint is HTTP, so HTTPS 
needs no stable port.
+    conf.set(OZONE_S3G_HTTP_ADDRESS_KEY, address(config.getHost(), 
s3gHttpPort));
+    conf.set(OZONE_S3G_HTTP_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_S3G_HTTPS_ADDRESS_KEY, address(config.getHost(), 0));
+    conf.set(OZONE_S3G_HTTPS_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_S3G_WEBADMIN_HTTP_ADDRESS_KEY, address(config.getHost(), 
s3gWebHttpPort));
+    conf.set(OZONE_S3G_WEBADMIN_HTTP_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_S3G_WEBADMIN_HTTPS_ADDRESS_KEY, address(config.getHost(), 
0));
+    conf.set(OZONE_S3G_WEBADMIN_HTTPS_BIND_HOST_KEY, config.getBindHost());
+    return s3gHttpPort;
+  }
+
   private List<OzoneConfiguration> configureDatanodes(OzoneConfiguration conf,
       PersistedPortState persistedPorts, PortAllocator portAllocator)
       throws IOException {
@@ -765,6 +832,18 @@ private void startDatanodes(List<OzoneConfiguration> 
datanodeConfigurations) {
     }
   }
 
+  private void startS3Gateway(OzoneConfiguration conf) throws Exception {
+    // Gateway reads the static holder; setConfiguration() keeps the first 
value.
+    OzoneConfigurationHolder.resetConfiguration();
+    OzoneConfigurationHolder.setConfiguration(new OzoneConfiguration(conf));
+    // Assigned first so start()'s stopServices() rollback stops a failed 
call().
+    s3Gateway = new Gateway();
+    // call(), not execute(): GenericCli#printError drops the cause and can 
exit the JVM.
+    // parseArgs() sets the parse result Gateway#start reads.
+    s3Gateway.getCmd().parseArgs();
+    s3Gateway.call();
+  }
+
   private void waitForClusterReadiness(Duration timeout) throws Exception {
     waitForReadiness(this::clusterReadinessBlocker, "Ozone cluster", timeout);
   }
@@ -938,6 +1017,10 @@ private PersistedPortState loadPersistedPortState() 
throws IOException {
   private String[] requiredPersistedPortKeys() {
     List<String> keys =
         new ArrayList<>(Arrays.asList(REQUIRED_PERSISTED_PORT_KEYS));
+    if (config.isS3gEnabled()) {
+      keys.add(S3G_HTTP_PORT_KEY);
+      keys.add(S3G_WEBADMIN_HTTP_PORT_KEY);
+    }
     for (int index = 0; index < config.getDatanodes(); index++) {
       for (String suffix : DATANODE_PORT_KEY_SUFFIXES) {
         keys.add(datanodePortKey(index, suffix));
@@ -1000,14 +1083,16 @@ static final class PreparedConfiguration {
     private final OzoneConfiguration configuration;
     private final int scmPort;
     private final int omPort;
+    private final int s3gPort;
     private final List<OzoneConfiguration> datanodeConfigurations;
 
-    PreparedConfiguration(OzoneConfiguration configuration, int scmPort,
-        int omPort, List<OzoneConfiguration> datanodeConfigurations) {
+    PreparedConfiguration(OzoneConfiguration configuration, int scmPort, int 
omPort, int s3gPort,
+        List<OzoneConfiguration> datanodeConfigurations) {
       this.configuration = Objects.requireNonNull(configuration,
           "configuration");
       this.scmPort = scmPort;
       this.omPort = omPort;
+      this.s3gPort = s3gPort;
       this.datanodeConfigurations = Collections.unmodifiableList(
           new ArrayList<>(Objects.requireNonNull(datanodeConfigurations,
               "datanodeConfigurations")));
@@ -1025,6 +1110,10 @@ int getOmPort() {
       return omPort;
     }
 
+    int getS3gPort() {
+      return s3gPort;
+    }
+
     List<OzoneConfiguration> getDatanodeConfigurations() {
       return datanodeConfigurations;
     }
@@ -1035,6 +1124,17 @@ List<OzoneConfiguration> getDatanodeConfigurations() {
    */
   static final class PortAllocator {
     private final Set<Integer> reserved = new HashSet<>();
+    private final Set<Integer> persisted;
+    private final CheckedSupplier<Integer, IOException> freePorts;
+
+    PortAllocator(Set<Integer> persisted) {
+      this(persisted, PortAllocator::nextFreePort);
+    }
+
+    PortAllocator(Set<Integer> persisted, CheckedSupplier<Integer, 
IOException> freePorts) {
+      this.persisted = persisted;
+      this.freePorts = freePorts;
+    }
 
     int reserve(int preferredPort) throws IOException {
       if (preferredPort > 0) {
@@ -1042,8 +1142,9 @@ int reserve(int preferredPort) throws IOException {
       }
 
       while (true) {
-        int candidate = nextFreePort();
-        if (reserved.add(candidate)) {
+        int candidate = freePorts.get();
+        // A persisted port is free while the cluster is down, but its key may 
reserve it later in this run.
+        if (!persisted.contains(candidate) && reserved.add(candidate)) {
           return candidate;
         }
       }
@@ -1120,6 +1221,14 @@ int get(String key) throws IOException {
       }
     }
 
+    Set<Integer> ports() throws IOException {
+      Set<Integer> ports = new HashSet<>();
+      for (String key : properties.stringPropertyNames()) {
+        ports.add(get(key));
+      }
+      return ports;
+    }
+
     void requireKeys(String[] keys) throws IOException {
       for (String key : keys) {
         if (get(key) <= 0) {
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
index 4a5d9d7ea20..e2d62d2f860 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
@@ -56,7 +56,7 @@ public final class LocalOzoneClusterConfig {
   static final int DEFAULT_DATANODES =
       Integer.parseInt(DEFAULT_DATANODES_VALUE);
   static final String DEFAULT_HOST = "127.0.0.1";
-  static final String DEFAULT_BIND_HOST = "0.0.0.0";
+  static final String WILDCARD_HOST = "0.0.0.0";
   static final int DEFAULT_PORT = Integer.parseInt(DEFAULT_PORT_VALUE);
   static final boolean DEFAULT_S3G_ENABLED =
       Boolean.parseBoolean(DEFAULT_S3G_ENABLED_VALUE);
@@ -64,9 +64,10 @@ public final class LocalOzoneClusterConfig {
       Boolean.parseBoolean(DEFAULT_EPHEMERAL_VALUE);
   static final Duration DEFAULT_STARTUP_TIMEOUT =
       Duration.parse(DEFAULT_STARTUP_TIMEOUT_VALUE);
-  static final String DEFAULT_S3_ACCESS_KEY = "admin";
-  static final String DEFAULT_S3_SECRET_KEY = "admin123";
-  static final String DEFAULT_S3_REGION = "us-east-1";
+  // Printed for S3 clients; not checked while security is off.
+  static final String LOCAL_S3_ACCESS_KEY = "admin";
+  static final String LOCAL_S3_SECRET_KEY = "admin123";
+  static final String LOCAL_S3_REGION = "us-east-1";
 
   private final Path dataDir;
   private final FormatMode formatMode;
@@ -79,9 +80,6 @@ public final class LocalOzoneClusterConfig {
   private final boolean s3gEnabled;
   private final boolean ephemeral;
   private final Duration startupTimeout;
-  private final String s3AccessKey;
-  private final String s3SecretKey;
-  private final String s3Region;
 
   private LocalOzoneClusterConfig(Builder builder) {
     dataDir = Objects.requireNonNull(builder.dataDir, "dataDir")
@@ -90,7 +88,8 @@ private LocalOzoneClusterConfig(Builder builder) {
     formatMode = Objects.requireNonNull(builder.formatMode, "formatMode");
     datanodes = builder.datanodes;
     host = Objects.requireNonNull(builder.host, "host");
-    bindHost = Objects.requireNonNull(builder.bindHost, "bindHost");
+    // Defaults to host rather than WILDCARD_HOST: the local runtime runs 
without security by default.
+    bindHost = builder.bindHost != null ? builder.bindHost : host;
     scmPort = builder.scmPort;
     omPort = builder.omPort;
     s3gPort = builder.s3gPort;
@@ -98,9 +97,6 @@ private LocalOzoneClusterConfig(Builder builder) {
     ephemeral = builder.ephemeral;
     startupTimeout = Objects.requireNonNull(builder.startupTimeout,
         "startupTimeout");
-    s3AccessKey = Objects.requireNonNull(builder.s3AccessKey, "s3AccessKey");
-    s3SecretKey = Objects.requireNonNull(builder.s3SecretKey, "s3SecretKey");
-    s3Region = Objects.requireNonNull(builder.s3Region, "s3Region");
   }
 
   public Path getDataDir() {
@@ -170,27 +166,6 @@ public Duration getStartupTimeout() {
     return startupTimeout;
   }
 
-  /**
-   * Returns the suggested local-only S3 access key printed for client setup.
-   */
-  public String getS3AccessKey() {
-    return s3AccessKey;
-  }
-
-  /**
-   * Returns the suggested local-only S3 secret key printed for client setup.
-   */
-  public String getS3SecretKey() {
-    return s3SecretKey;
-  }
-
-  /**
-   * Returns the suggested local-only S3 region printed for client setup.
-   */
-  public String getS3Region() {
-    return s3Region;
-  }
-
   public static Builder builder() {
     return new Builder(DEFAULT_DATA_DIR);
   }
@@ -240,16 +215,13 @@ public static final class Builder {
     private FormatMode formatMode = DEFAULT_FORMAT_MODE;
     private int datanodes = DEFAULT_DATANODES;
     private String host = DEFAULT_HOST;
-    private String bindHost = DEFAULT_BIND_HOST;
+    private String bindHost;
     private int scmPort = DEFAULT_PORT;
     private int omPort = DEFAULT_PORT;
     private int s3gPort = DEFAULT_PORT;
     private boolean s3gEnabled = DEFAULT_S3G_ENABLED;
     private boolean ephemeral = DEFAULT_EPHEMERAL;
     private Duration startupTimeout = DEFAULT_STARTUP_TIMEOUT;
-    private String s3AccessKey = DEFAULT_S3_ACCESS_KEY;
-    private String s3SecretKey = DEFAULT_S3_SECRET_KEY;
-    private String s3Region = DEFAULT_S3_REGION;
 
     private Builder(Path dataDir) {
       this.dataDir = dataDir;
@@ -305,21 +277,6 @@ public Builder setStartupTimeout(Duration value) {
       return this;
     }
 
-    public Builder setS3AccessKey(String value) {
-      s3AccessKey = value;
-      return this;
-    }
-
-    public Builder setS3SecretKey(String value) {
-      s3SecretKey = value;
-      return this;
-    }
-
-    public Builder setS3Region(String value) {
-      s3Region = value;
-      return this;
-    }
-
     public LocalOzoneClusterConfig build() {
       return new LocalOzoneClusterConfig(this);
     }
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
index 3ed93904417..ed0176bd234 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
@@ -60,14 +60,14 @@ public interface LocalOzoneRuntime extends AutoCloseable {
   /**
    * Returns the S3 Gateway HTTP port for this local runtime.
    *
-   * @return S3 Gateway port
+   * @return S3 Gateway port, or -1 when the S3 Gateway is not running
    */
   int getS3gPort();
 
   /**
    * Returns the full S3 Gateway endpoint shown to users.
    *
-   * @return S3 Gateway endpoint, including scheme, host, and port
+   * @return S3 Gateway endpoint, including scheme, host, and port; empty when 
the S3 Gateway is not running
    */
   String getS3Endpoint();
 
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
index 79b50efc729..f2565361148 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
@@ -62,9 +62,6 @@ public class OzoneLocal extends GenericCli {
   static final String ENV_S3G_PORT = "OZONE_LOCAL_S3G_PORT";
   static final String ENV_EPHEMERAL = "OZONE_LOCAL_EPHEMERAL";
   static final String ENV_STARTUP_TIMEOUT = "OZONE_LOCAL_STARTUP_TIMEOUT";
-  static final String ENV_S3_ACCESS_KEY = "OZONE_LOCAL_S3_ACCESS_KEY";
-  static final String ENV_S3_SECRET_KEY = "OZONE_LOCAL_S3_SECRET_KEY";
-  static final String ENV_S3_REGION = "OZONE_LOCAL_S3_REGION";
 
   private static final String DEFAULT_DATA_DIR_VALUE = "${env:" + ENV_DATA_DIR
       + ":-" + LocalOzoneClusterConfig.DEFAULT_DATA_DIR_VALUE + "}";
@@ -75,8 +72,7 @@ public class OzoneLocal extends GenericCli {
       + "}";
   private static final String DEFAULT_HOST_VALUE = "${env:" + ENV_HOST
       + ":-" + LocalOzoneClusterConfig.DEFAULT_HOST + "}";
-  private static final String DEFAULT_BIND_HOST_VALUE = "${env:"
-      + ENV_BIND_HOST + ":-" + LocalOzoneClusterConfig.DEFAULT_BIND_HOST + "}";
+  private static final String DEFAULT_BIND_HOST_VALUE = "${env:" + 
ENV_BIND_HOST + "}";
   private static final String DEFAULT_SCM_PORT_VALUE = "${env:"
       + ENV_SCM_PORT + ":-" + LocalOzoneClusterConfig.DEFAULT_PORT_VALUE
       + "}";
@@ -93,14 +89,6 @@ public class OzoneLocal extends GenericCli {
   private static final String DEFAULT_STARTUP_TIMEOUT_VALUE = "${env:"
       + ENV_STARTUP_TIMEOUT + ":-"
       + LocalOzoneClusterConfig.DEFAULT_STARTUP_TIMEOUT_VALUE + "}";
-  private static final String DEFAULT_S3_ACCESS_KEY_VALUE = "${env:"
-      + ENV_S3_ACCESS_KEY + ":-"
-      + LocalOzoneClusterConfig.DEFAULT_S3_ACCESS_KEY + "}";
-  private static final String DEFAULT_S3_SECRET_KEY_VALUE = "${env:"
-      + ENV_S3_SECRET_KEY + ":-"
-      + LocalOzoneClusterConfig.DEFAULT_S3_SECRET_KEY + "}";
-  private static final String DEFAULT_S3_REGION_VALUE = "${env:"
-      + ENV_S3_REGION + ":-" + LocalOzoneClusterConfig.DEFAULT_S3_REGION + "}";
 
   public OzoneLocal() {
     super();
@@ -162,7 +150,7 @@ private StartupFailureException(Throwable cause) {
   }
 
   @Command(name = "run",
-      description = "Start SCM, OM, and datanodes in one local process")
+      description = "Start SCM, OM, datanodes, and optional S3 Gateway in one 
local process")
   static class RunCommand extends AbstractSubcommand implements Callable<Void> 
{
 
     @Option(names = "--data-dir",
@@ -188,7 +176,7 @@ static class RunCommand extends AbstractSubcommand 
implements Callable<Void> {
 
     @Option(names = "--bind-host",
         defaultValue = DEFAULT_BIND_HOST_VALUE,
-        description = "Bind host for HTTP and RPC listeners")
+        description = "Bind host for HTTP and RPC listeners (default: --host)")
     private String bindHost;
 
     @Option(names = "--scm-port",
@@ -226,21 +214,6 @@ static class RunCommand extends AbstractSubcommand 
implements Callable<Void> {
         description = "How long to wait for the local cluster to become ready")
     private Duration startupTimeout;
 
-    @Option(names = "--s3-access-key",
-        defaultValue = DEFAULT_S3_ACCESS_KEY_VALUE,
-        description = "Suggested local AWS access key to print on startup")
-    private String s3AccessKey;
-
-    @Option(names = "--s3-secret-key",
-        defaultValue = DEFAULT_S3_SECRET_KEY_VALUE,
-        description = "Suggested local AWS secret key to print on startup")
-    private String s3SecretKey;
-
-    @Option(names = "--s3-region",
-        defaultValue = DEFAULT_S3_REGION_VALUE,
-        description = "Suggested local AWS region to print on startup")
-    private String s3Region;
-
     @Override
     public Void call() throws Exception {
       LocalOzoneClusterConfig config = resolveConfig();
@@ -270,6 +243,16 @@ private void printSummary(LocalOzoneRuntime runtime, 
LocalOzoneClusterConfig con
       writer.println("Local Ozone is running from " + config.getDataDir());
       writer.println("SCM RPC: " + runtime.getDisplayHost() + ":" + 
runtime.getScmPort());
       writer.println("OM RPC: " + runtime.getDisplayHost() + ":" + 
runtime.getOmPort());
+      if (config.isS3gEnabled()) {
+        writer.println("S3 endpoint: " + runtime.getS3Endpoint());
+        writer.println("Suggested local AWS settings:");
+        writer.println("AWS_ACCESS_KEY_ID=" + 
LocalOzoneClusterConfig.LOCAL_S3_ACCESS_KEY);
+        writer.println("AWS_SECRET_ACCESS_KEY=" + 
LocalOzoneClusterConfig.LOCAL_S3_SECRET_KEY);
+        writer.println("AWS_REGION=" + 
LocalOzoneClusterConfig.LOCAL_S3_REGION);
+        writer.println("AWS_ENDPOINT_URL_S3=" + runtime.getS3Endpoint());
+        writer.println("Use path-style addressing (AWS CLI: aws configure set 
default.s3.addressing_style path).");
+        writer.println("With security off (the default), the S3 Gateway 
accepts any credentials.");
+      }
       writer.println("Press Ctrl+C to stop.");
       writer.flush();
     }
@@ -322,9 +305,6 @@ LocalOzoneClusterConfig resolveConfig() {
           .setS3gEnabled(s3gEnabled)
           .setEphemeral(ephemeral)
           .setStartupTimeout(startupTimeout)
-          .setS3AccessKey(s3AccessKey)
-          .setS3SecretKey(s3SecretKey)
-          .setS3Region(s3Region)
           .build();
     }
 
diff --git 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
index c0d2e19aea5..6da5f287e24 100644
--- 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
+++ 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.ozone.local;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_DATANODE_CLIENT_BIND_HOST_KEY;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_INTERVAL;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
@@ -25,18 +26,27 @@
 import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_CONTAINER_RATIS_ENABLED_KEY;
 import static org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_DATANODE_DIR_KEY;
 import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_BIND_HOST_KEY;
 import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_NAMES;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.HDDS_CONTAINER_IPC_PORT;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_HTTP_POLICY_KEY;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_METADATA_DIRS;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION;
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION_TYPE;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_BIND_HOST_KEY;
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_MINIMUM_TIMEOUT_KEY;
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
 import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_BIND_HOST_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ENABLED_KEY;
+import static 
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -48,6 +58,7 @@
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Properties;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -55,8 +66,11 @@
 import org.apache.hadoop.hdds.client.ReplicationType;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.SafeModeRuleStatusProto;
+import org.apache.hadoop.hdds.server.http.HttpConfig;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 /**
  * Tests for {@link LocalOzoneCluster}.
@@ -83,9 +97,23 @@ void prepareConfigurationExposesPreparedPorts() throws 
Exception {
       assertTrue(Files.isRegularFile(portStateFile(dataDir)));
       assertTrue(prepared.getScmPort() > 0);
       assertTrue(prepared.getOmPort() > 0);
+      assertTrue(prepared.getS3gPort() > 0);
+    }
+  }
+
+  @Test
+  void prepareConfigurationSkipsS3GatewayWhenDisabled() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig config = 
LocalOzoneClusterConfig.builder(dataDir).setS3gEnabled(false).build();
+
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      LocalOzoneCluster.PreparedConfiguration prepared = 
cluster.prepareConfiguration();
+
+      assertEquals(-1, prepared.getS3gPort());
       assertEquals(-1, cluster.getS3gPort());
       assertEquals("", cluster.getS3Endpoint());
     }
+    
assertFalse(loadPortState(dataDir).stringPropertyNames().stream().anyMatch(key 
-> key.startsWith("s3g.")));
   }
 
   @Test
@@ -95,6 +123,7 @@ void prepareConfigurationCreatesBaseLayoutAndLocalDefaults()
     LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
         .setScmPort(9860)
         .setOmPort(9862)
+        .setS3gPort(9878)
         .setDatanodes(2)
         .build();
 
@@ -116,10 +145,67 @@ void 
prepareConfigurationCreatesBaseLayoutAndLocalDefaults()
     assertEquals(2, conf.getInt(HDDS_SCM_SAFEMODE_MIN_DATANODE, 0));
     assertTrue(conf.get(OZONE_SCM_CLIENT_ADDRESS_KEY).endsWith(":9860"));
     assertTrue(conf.get(OZONE_OM_ADDRESS_KEY).endsWith(":9862"));
+    assertTrue(conf.get(OZONE_S3G_HTTP_ADDRESS_KEY).endsWith(":9878"));
+    assertTrue(conf.get(OZONE_S3G_HTTPS_ADDRESS_KEY).endsWith(":0"));
+    
assertTrue(conf.getBoolean(OZONE_S3G_STANDARD_STORAGE_CLASS_USE_CLIENT_DEFAULT_KEY,
 false));
     assertTrue(conf.getTrimmedStringCollection(OZONE_SCM_NAMES).iterator()
         .next().contains(":"));
     assertEquals(9860, prepared.getScmPort());
     assertEquals(9862, prepared.getOmPort());
+    assertEquals(9878, prepared.getS3gPort());
+  }
+
+  @Test
+  void s3AccessorsReportNoListenerBeforeStart() throws Exception {
+    LocalOzoneClusterConfig config = defaultConfig();
+
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      assertTrue(config.isS3gEnabled());
+      assertEquals(-1, cluster.getS3gPort());
+      assertEquals("", cluster.getS3Endpoint());
+      assertNull(cluster.getS3gBoundAddress());
+    }
+  }
+
+  @Test
+  void prepareConfigurationRejectsDisabledS3GatewayHttpListener() {
+    OzoneConfiguration seed = new OzoneConfiguration();
+    seed.setBoolean(OZONE_S3G_HTTP_ENABLED_KEY, false);
+
+    IOException error = assertPrepareFails(seed);
+
+    assertMessageContains(error, OZONE_S3G_HTTP_ENABLED_KEY);
+  }
+
+  @ParameterizedTest
+  @EnumSource(HttpConfig.Policy.class)
+  void prepareConfigurationChecksHttpPolicyForS3Gateway(HttpConfig.Policy 
policy) throws Exception {
+    OzoneConfiguration seed = new OzoneConfiguration();
+    seed.set(OZONE_HTTP_POLICY_KEY, policy.name(), "test-ozone-site.xml");
+
+    if (policy.isHttpEnabled()) {
+      prepared(seed);
+    } else {
+      IOException error = assertPrepareFails(seed);
+      assertMessageContains(error, OZONE_HTTP_POLICY_KEY);
+      assertMessageContains(error, "test-ozone-site.xml");
+    }
+    LocalOzoneClusterConfig withoutS3g =
+        
LocalOzoneClusterConfig.builder(tempDir.resolve("without-s3g")).setS3gEnabled(false).build();
+    try (LocalOzoneCluster cluster = new LocalOzoneCluster(withoutS3g, seed)) {
+      cluster.prepareConfiguration();
+    }
+  }
+
+  @Test
+  void prepareConfigurationBindsListenersToLoopbackByDefault() throws 
Exception {
+    LocalOzoneCluster.PreparedConfiguration prepared = 
prepare(defaultConfig());
+    OzoneConfiguration conf = prepared.getConfiguration();
+
+    assertEquals("127.0.0.1", conf.get(OZONE_SCM_CLIENT_BIND_HOST_KEY));
+    assertEquals("127.0.0.1", conf.get(OZONE_OM_HTTP_BIND_HOST_KEY));
+    assertEquals("127.0.0.1", conf.get(OZONE_S3G_HTTP_BIND_HOST_KEY));
+    assertEquals("127.0.0.1", 
prepared.getDatanodeConfigurations().get(0).get(HDDS_DATANODE_CLIENT_BIND_HOST_KEY));
   }
 
   @Test
@@ -312,10 +398,35 @@ void persistedPortFileContainsDistinctAllocatedPorts() 
throws Exception {
     assertPositivePort(properties, "om.rpc");
     assertPositivePort(properties, "om.http");
     assertPositivePort(properties, "om.ratis");
+    assertPositivePort(properties, "s3g.http");
+    assertPositivePort(properties, "s3g.web.http");
     assertNotEquals(properties.getProperty("scm.client"),
         properties.getProperty("om.rpc"));
   }
 
+  @Test
+  void freshPortSkipsPersistedPorts() throws Exception {
+    Iterator<Integer> freePorts = Arrays.asList(40000, 40001).iterator();
+    LocalOzoneCluster.PortAllocator allocator =
+        new LocalOzoneCluster.PortAllocator(Collections.singleton(40000), 
freePorts::next);
+
+    assertEquals(40001, allocator.reserve(0));
+    assertEquals(40000, allocator.reserve(40000));
+  }
+
+  @Test
+  void formatNeverRejectsPortStateMissingS3GatewayPorts() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    
prepare(LocalOzoneClusterConfig.builder(dataDir).setS3gEnabled(false).build());
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+        .build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, "s3g.");
+  }
+
   @Test
   void prepareConfigurationCreatesDatanodeConfigurations() throws Exception {
     Path dataDir = tempDir.resolve("local-ozone");
diff --git 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
index 7e7f8248024..1f9fddbb965 100644
--- 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
+++ 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
@@ -41,16 +41,13 @@ void builderProvidesLocalClusterDefaults() {
         config.getFormatMode());
     assertEquals(1, config.getDatanodes());
     assertEquals("127.0.0.1", config.getHost());
-    assertEquals("0.0.0.0", config.getBindHost());
+    assertEquals("127.0.0.1", config.getBindHost());
     assertEquals(0, config.getScmPort());
     assertEquals(0, config.getOmPort());
     assertEquals(0, config.getS3gPort());
     assertTrue(config.isS3gEnabled());
     assertFalse(config.isEphemeral());
     assertEquals(Duration.ofMinutes(2), config.getStartupTimeout());
-    assertEquals("admin", config.getS3AccessKey());
-    assertEquals("admin123", config.getS3SecretKey());
-    assertEquals("us-east-1", config.getS3Region());
   }
 
   @Test
@@ -88,9 +85,6 @@ void builderAcceptsExplicitOverrides() {
         .setS3gEnabled(false)
         .setEphemeral(true)
         .setStartupTimeout(Duration.ofSeconds(45))
-        .setS3AccessKey("dev")
-        .setS3SecretKey("secret")
-        .setS3Region("ap-south-1")
         .build();
 
     assertEquals(Paths.get("target", "custom-local-ozone")
@@ -106,9 +100,6 @@ void builderAcceptsExplicitOverrides() {
     assertFalse(config.isS3gEnabled());
     assertTrue(config.isEphemeral());
     assertEquals(Duration.ofSeconds(45), config.getStartupTimeout());
-    assertEquals("dev", config.getS3AccessKey());
-    assertEquals("secret", config.getS3SecretKey());
-    assertEquals("ap-south-1", config.getS3Region());
   }
 
   @Test
diff --git 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
index c91c30d563a..9427be1d502 100644
--- 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
+++ 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
@@ -101,7 +101,7 @@ void rootHelpListsRunCommand() throws Exception {
   @Test
   void runCommandStartsRuntimeAndPrintsStartupSummary() throws Exception {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
-    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862);
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862, 
"http://localhost:9878";);
     TestableRunCommand command = new TestableRunCommand(runtime);
     CommandLine commandLine = new CommandLine(command);
     commandLine.setOut(new PrintWriter(new OutputStreamWriter(out, UTF_8),
@@ -117,12 +117,36 @@ void runCommandStartsRuntimeAndPrintsStartupSummary() 
throws Exception {
     assertTrue(text.contains("SCM RPC: localhost:9860"), text);
     assertTrue(text.contains("OM RPC: localhost:9862"), text);
     assertFalse(text.contains("Datanodes:"), text);
+    assertTrue(text.contains("S3 endpoint: http://localhost:9878";), text);
+    assertTrue(text.contains("AWS_ACCESS_KEY_ID=" + 
LocalOzoneClusterConfig.LOCAL_S3_ACCESS_KEY), text);
+    assertTrue(text.contains("AWS_SECRET_ACCESS_KEY=" + 
LocalOzoneClusterConfig.LOCAL_S3_SECRET_KEY), text);
+    assertTrue(text.contains("AWS_REGION=" + 
LocalOzoneClusterConfig.LOCAL_S3_REGION), text);
+    assertTrue(text.contains("AWS_ENDPOINT_URL_S3=http://localhost:9878";), 
text);
+    assertTrue(text.contains("aws configure set default.s3.addressing_style 
path"), text);
+    assertTrue(text.contains("accepts any credentials"), text);
+    assertTrue(text.contains("Press Ctrl+C to stop."), text);
+  }
+
+  @Test
+  void runCommandOmitsS3SummaryWhenS3gDisabled() throws Exception {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862, "");
+    TestableRunCommand command = new TestableRunCommand(runtime);
+    CommandLine commandLine = new CommandLine(command);
+    commandLine.setOut(new PrintWriter(new OutputStreamWriter(out, UTF_8), 
true));
+
+    int exitCode = commandLine.execute("--no-s3g");
+
+    assertEquals(0, exitCode);
+    String text = out.toString(UTF_8.name());
+    assertFalse(text.contains("S3 endpoint:"), text);
+    assertFalse(text.contains("AWS_ACCESS_KEY_ID="), text);
     assertTrue(text.contains("Press Ctrl+C to stop."), text);
   }
 
   @Test
   void runCommandClosesRuntimeWhenStartupFails() {
-    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862);
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862, "");
     runtime.failStart = true;
     TestableRunCommand command = new TestableRunCommand(runtime);
 
@@ -135,7 +159,7 @@ void runCommandClosesRuntimeWhenStartupFails() {
   @Test
   void runCommandPreservesStartupFailureAsTheCause() throws Exception {
     ByteArrayOutputStream err = new ByteArrayOutputStream();
-    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862);
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862, "");
     runtime.failStart = true;
     TestableRunCommand command = new TestableRunCommand(runtime);
     CommandLine commandLine = new CommandLine(command);
@@ -243,8 +267,8 @@ void runCommandOptionsUseEnvironmentDefaults() throws 
Exception {
         LocalOzoneClusterConfig.DEFAULT_DATANODES_VALUE);
     assertEnvDefault("host", OzoneLocal.ENV_HOST,
         LocalOzoneClusterConfig.DEFAULT_HOST);
-    assertEnvDefault("bindHost", OzoneLocal.ENV_BIND_HOST,
-        LocalOzoneClusterConfig.DEFAULT_BIND_HOST);
+    assertEquals("${env:" + OzoneLocal.ENV_BIND_HOST + "}",
+        
OzoneLocal.RunCommand.class.getDeclaredField("bindHost").getAnnotation(Option.class).defaultValue());
     assertEnvDefault("scmPort", OzoneLocal.ENV_SCM_PORT,
         LocalOzoneClusterConfig.DEFAULT_PORT_VALUE);
     assertEnvDefault("omPort", OzoneLocal.ENV_OM_PORT,
@@ -257,12 +281,6 @@ void runCommandOptionsUseEnvironmentDefaults() throws 
Exception {
         LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE);
     assertEnvDefault("startupTimeout", OzoneLocal.ENV_STARTUP_TIMEOUT,
         LocalOzoneClusterConfig.DEFAULT_STARTUP_TIMEOUT_VALUE);
-    assertEnvDefault("s3AccessKey", OzoneLocal.ENV_S3_ACCESS_KEY,
-        LocalOzoneClusterConfig.DEFAULT_S3_ACCESS_KEY);
-    assertEnvDefault("s3SecretKey", OzoneLocal.ENV_S3_SECRET_KEY,
-        LocalOzoneClusterConfig.DEFAULT_S3_SECRET_KEY);
-    assertEnvDefault("s3Region", OzoneLocal.ENV_S3_REGION,
-        LocalOzoneClusterConfig.DEFAULT_S3_REGION);
   }
 
   @Test
@@ -275,16 +293,18 @@ void resolveConfigUsesPicocliDefaults() {
         config.getFormatMode());
     assertEquals(1, config.getDatanodes());
     assertEquals("127.0.0.1", config.getHost());
-    assertEquals("0.0.0.0", config.getBindHost());
+    assertEquals("127.0.0.1", config.getBindHost());
     assertEquals(0, config.getScmPort());
     assertEquals(0, config.getOmPort());
     assertEquals(0, config.getS3gPort());
     assertTrue(config.isS3gEnabled());
     assertFalse(config.isEphemeral());
     assertEquals(Duration.ofMinutes(2), config.getStartupTimeout());
-    assertEquals("admin", config.getS3AccessKey());
-    assertEquals("admin123", config.getS3SecretKey());
-    assertEquals("us-east-1", config.getS3Region());
+  }
+
+  @Test
+  void resolveConfigBindsToHostWithoutBindHost() {
+    assertEquals("cli-host", resolve("--host", "cli-host").getBindHost());
   }
 
   @Test
@@ -294,16 +314,13 @@ void resolveConfigUsesCliOverrides() {
         "--format", "always",
         "--datanodes", "3",
         "--host", "cli-host",
-        "--bind-host", "127.0.0.1",
+        "--bind-host", "0.0.0.0",
         "--scm-port", "200",
         "--om-port", "201",
         "--s3g-port", "202",
         "--no-s3g",
         "--ephemeral",
-        "--startup-timeout", "45s",
-        "--s3-access-key", "cli-access",
-        "--s3-secret-key", "cli-secret",
-        "--s3-region", "cli-region");
+        "--startup-timeout", "45s");
 
     assertEquals(Paths.get("target/cli-local").toAbsolutePath().normalize(),
         config.getDataDir());
@@ -311,16 +328,13 @@ void resolveConfigUsesCliOverrides() {
         config.getFormatMode());
     assertEquals(3, config.getDatanodes());
     assertEquals("cli-host", config.getHost());
-    assertEquals("127.0.0.1", config.getBindHost());
+    assertEquals("0.0.0.0", config.getBindHost());
     assertEquals(200, config.getScmPort());
     assertEquals(201, config.getOmPort());
     assertEquals(202, config.getS3gPort());
     assertFalse(config.isS3gEnabled());
     assertTrue(config.isEphemeral());
     assertEquals(Duration.ofSeconds(45), config.getStartupTimeout());
-    assertEquals("cli-access", config.getS3AccessKey());
-    assertEquals("cli-secret", config.getS3SecretKey());
-    assertEquals("cli-region", config.getS3Region());
   }
 
   @Test
@@ -484,14 +498,16 @@ private static final class StubRuntime implements 
LocalOzoneRuntime {
     private final String displayHost;
     private final int scmPort;
     private final int omPort;
+    private final String s3Endpoint;
     private boolean failStart;
     private boolean started;
     private boolean closed;
 
-    private StubRuntime(String displayHost, int scmPort, int omPort) {
+    private StubRuntime(String displayHost, int scmPort, int omPort, String 
s3Endpoint) {
       this.displayHost = displayHost;
       this.scmPort = scmPort;
       this.omPort = omPort;
+      this.s3Endpoint = s3Endpoint;
     }
 
     @Override
@@ -524,7 +540,7 @@ public int getS3gPort() {
 
     @Override
     public String getS3Endpoint() {
-      return "";
+      return s3Endpoint;
     }
 
     @Override
@@ -550,8 +566,6 @@ public String defaultValue(ArgSpec argSpec) {
         return LocalOzoneClusterConfig.DEFAULT_DATANODES_VALUE;
       } else if ("--host".equals(option)) {
         return LocalOzoneClusterConfig.DEFAULT_HOST;
-      } else if ("--bind-host".equals(option)) {
-        return LocalOzoneClusterConfig.DEFAULT_BIND_HOST;
       } else if ("--scm-port".equals(option)
           || "--om-port".equals(option)
           || "--s3g-port".equals(option)) {
@@ -562,12 +576,6 @@ public String defaultValue(ArgSpec argSpec) {
         return LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE;
       } else if ("--startup-timeout".equals(option)) {
         return LocalOzoneClusterConfig.DEFAULT_STARTUP_TIMEOUT_VALUE;
-      } else if ("--s3-access-key".equals(option)) {
-        return LocalOzoneClusterConfig.DEFAULT_S3_ACCESS_KEY;
-      } else if ("--s3-secret-key".equals(option)) {
-        return LocalOzoneClusterConfig.DEFAULT_S3_SECRET_KEY;
-      } else if ("--s3-region".equals(option)) {
-        return LocalOzoneClusterConfig.DEFAULT_S3_REGION;
       }
       return null;
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to