diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index f08e03c03eb..38a660b4a4f 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -1757,6 +1757,14 @@
service principal.
+
+ ozone.s3g.readonly
+ false
+ OZONE, S3GATEWAY
+ Whether the S3Gateway blocks PUT/POST/DELETE methods or not.
+ Mostly used for system maintenance.
+
+
ozone.om.save.metrics.interval
5m
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java
index 3a63a593469..4d156b198fe 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java
@@ -92,11 +92,19 @@ public OzoneClient(ConfigurationSource conf, ClientProtocol proxy) {
@VisibleForTesting
protected OzoneClient(ObjectStore objectStore,
ClientProtocol clientProtocol) {
+ this(objectStore, clientProtocol, new OzoneConfiguration());
+ }
+
+ @VisibleForTesting
+ protected OzoneClient(ObjectStore objectStore,
+ ClientProtocol clientProtocol,
+ OzoneConfiguration conf) {
this.objectStore = objectStore;
this.proxy = clientProtocol;
// For the unit test
- this.conf = new OzoneConfiguration();
+ this.conf = conf;
}
+
/**
* Returns the object store associated with the Ozone Cluster.
* @return ObjectStore
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 b8153f0209d..f4de110a4c2 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
@@ -41,6 +41,7 @@
import static org.apache.hadoop.ozone.conf.OzoneServiceConfig.DEFAULT_SHUTDOWN_HOOK_PRIORITY;
import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_KERBEROS_KEYTAB_FILE_KEY;
import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_KERBEROS_PRINCIPAL_KEY;
+import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_READONLY;
/**
* This class is used to start/stop S3 compatible rest server.
@@ -92,6 +93,8 @@ public void start() throws IOException {
LOG.info("Starting Ozone S3 gateway");
HddsServerUtil.initializeMetrics(ozoneConfiguration, "S3Gateway");
+ LOG.info("S3 Gateway Readonly mode: {}={}", OZONE_S3G_READONLY,
+ ozoneConfiguration.get(OZONE_S3G_READONLY));
httpServer.start();
}
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 af8575300e1..56d1162c7cc 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
@@ -64,7 +64,8 @@ public final class S3GatewayConfigKeys {
public static final String OZONE_S3G_KERBEROS_PRINCIPAL_KEY =
"ozone.s3g.kerberos.principal";
-
+ public static final String OZONE_S3G_READONLY = "ozone.s3g.readonly";
+ public static final boolean OZONE_S3G_READONLY_DEFAULT = false;
/**
* Never constructed.
*/
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index 6bbd1a5b30a..3f1bc561058 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -39,6 +39,7 @@
import org.apache.hadoop.ozone.s3.util.ContinueToken;
import org.apache.hadoop.ozone.s3.util.S3StorageType;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
+
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +65,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import static org.apache.hadoop.ozone.OzoneAcl.AclScope.ACCESS;
@@ -243,6 +245,12 @@ public Response put(@PathParam("bucket") String bucketName,
InputStream body) throws IOException, OS3Exception {
S3GAction s3GAction = S3GAction.CREATE_BUCKET;
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
try {
if (aclMarker != null) {
s3GAction = S3GAction.PUT_ACL;
@@ -347,6 +355,12 @@ public Response delete(@PathParam("bucket") String bucketName)
throws IOException, OS3Exception {
S3GAction s3GAction = S3GAction.DELETE_BUCKET;
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
try {
deleteS3Bucket(bucketName);
} catch (OMException ex) {
@@ -390,9 +404,18 @@ public MultiDeleteResponse multiDelete(@PathParam("bucket") String bucketName,
MultiDeleteRequest request)
throws OS3Exception, IOException {
S3GAction s3GAction = S3GAction.MULTI_DELETE;
+ MultiDeleteResponse result = new MultiDeleteResponse();
+
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ Response res = checkResult.get();
+ result.addError(new Error("", res.getStatusInfo().getReasonPhrase(),
+ "The S3Gateway is in read-only mode."));
+ }
OzoneBucket bucket = getBucket(bucketName);
- MultiDeleteResponse result = new MultiDeleteResponse();
+
if (request.getObjects() != null) {
for (DeleteObject keyToDelete : request.getObjects()) {
try {
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 56a187fd004..cfdc357979e 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
@@ -24,14 +24,15 @@
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
import java.io.IOException;
-import java.util.Set;
-import java.util.HashSet;
import java.util.Arrays;
-import java.util.Map;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -51,6 +52,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
import org.apache.hadoop.ozone.om.protocol.S3Auth;
+import org.apache.hadoop.ozone.s3.S3GatewayConfigKeys;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
@@ -58,6 +60,7 @@
import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
import org.apache.hadoop.ozone.s3.util.AuditUtils;
+import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -380,4 +383,14 @@ protected boolean isAccessDenied(OMException ex) {
|| result == ResultCodes.INVALID_TOKEN;
}
+ protected Optional checkIfReadonly() {
+ // Check if the S3Gateway is in read-only mode or not.
+ if (getClient().getConfiguration().getBoolean(
+ S3GatewayConfigKeys.OZONE_S3G_READONLY,
+ S3GatewayConfigKeys.OZONE_S3G_READONLY_DEFAULT)) {
+ return Optional.of(Response.status(HttpStatus.SC_METHOD_NOT_ALLOWED).
+ header("Allow", "GET,HEAD").build());
+ }
+ return Optional.empty();
+ }
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index 488dc9ce492..78b6ac30352 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -48,9 +48,10 @@
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
-import java.util.Map;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.OptionalLong;
import org.apache.commons.lang3.StringUtils;
@@ -175,6 +176,12 @@ public Response put(
@QueryParam("uploadId") @DefaultValue("") String uploadID,
InputStream body) throws IOException, OS3Exception {
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
S3GAction s3GAction = S3GAction.CREATE_KEY;
boolean auditSuccess = true;
@@ -527,6 +534,12 @@ public Response delete(
@QueryParam("uploadId") @DefaultValue("") String uploadId) throws
IOException, OS3Exception {
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
S3GAction s3GAction = S3GAction.DELETE_KEY;
try {
@@ -591,6 +604,13 @@ public Response initializeMultipartUpload(
@PathParam("path") String key
)
throws IOException, OS3Exception {
+
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
S3GAction s3GAction = S3GAction.INIT_MULTIPART_UPLOAD;
try {
@@ -658,6 +678,13 @@ public Response completeMultipartUpload(@PathParam("bucket") String bucket,
@QueryParam("uploadId") @DefaultValue("") String uploadID,
CompleteMultipartUploadRequest multipartUploadRequest)
throws IOException, OS3Exception {
+
+ // Check if the S3Gateway status is readonly
+ Optional checkResult = checkIfReadonly();
+ if (checkResult.isPresent()) {
+ return checkResult.get();
+ }
+
S3GAction s3GAction = S3GAction.COMPLETE_MULTIPART_UPLOAD;
OzoneVolume volume = getVolume();
// Using LinkedHashMap to preserve ordering of parts list.
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java
index 64f515060b4..84198b9e035 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java
@@ -19,6 +19,8 @@
*/
package org.apache.hadoop.ozone.client;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+
/**
* In-memory OzoneClient for testing.
*/
@@ -31,6 +33,11 @@ public OzoneClientStub(ObjectStoreStub objectStoreStub) {
super(objectStoreStub, new ClientProtocolStub(objectStoreStub));
}
+ public OzoneClientStub(ObjectStoreStub objectStoreStub,
+ OzoneConfiguration conf) {
+ super(objectStoreStub, new ClientProtocolStub(objectStoreStub), conf);
+ }
+
@Override
public void close() {
//NOOP.
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java
new file mode 100644
index 00000000000..418b1171014
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java
@@ -0,0 +1,188 @@
+/*
+ * 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.s3.endpoint;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.client.ObjectStoreStub;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.client.io.OzoneInputStream;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.hadoop.ozone.s3.S3GatewayConfigKeys;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;;
+import javax.ws.rs.core.UriInfo;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.format.DateTimeFormatter;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Test put object.
+ */
+public class TestReadonlyEndpoint {
+ public static final String CONTENT = "0123456789";
+ private String bucketName = "b1";
+ private String keyName = "k1/1";
+ private String destBucket = "b2";
+ private OzoneClient clientStub;
+ private ObjectEndpoint objectEndpoint;
+ private BucketEndpoint bucketEndpoint;
+
+ private HttpHeaders headers;
+ private ByteArrayInputStream body;
+ private ContainerRequestContext context;
+
+ @Before
+ public void setup() throws IOException {
+ //Create client stub and object store stub.
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(S3GatewayConfigKeys.OZONE_S3G_READONLY, true);
+ ObjectStoreStub objectStoreStub = new ObjectStoreStub();
+ clientStub = new OzoneClientStub(objectStoreStub, conf);
+
+ // Create bucket
+ clientStub.getObjectStore().createS3Bucket(bucketName);
+ clientStub.getObjectStore().createS3Bucket(destBucket);
+
+ // Create PutObject and setClient to OzoneClientStub
+ objectEndpoint = new ObjectEndpoint();
+ objectEndpoint.setClient(clientStub);
+ objectEndpoint.setOzoneConfiguration(new OzoneConfiguration());
+
+ OzoneBucket bucket = clientStub.getObjectStore().getS3Bucket(bucketName);
+ OzoneOutputStream keyStream =
+ bucket.createKey("key1", CONTENT.getBytes(UTF_8).length);
+ keyStream.write(CONTENT.getBytes(UTF_8));
+ keyStream.close();
+
+ // Return data for get
+ headers = Mockito.mock(HttpHeaders.class);
+ objectEndpoint.setHeaders(headers);
+ body = new ByteArrayInputStream(CONTENT.getBytes(UTF_8));
+
+ context = Mockito.mock(ContainerRequestContext.class);
+ Mockito.when(context.getUriInfo()).thenReturn(Mockito.mock(UriInfo.class));
+ Mockito.when(context.getUriInfo().getQueryParameters())
+ .thenReturn(new MultivaluedHashMap<>());
+ objectEndpoint.setContext(context);
+
+ bucketEndpoint = new BucketEndpoint();
+ bucketEndpoint.setClient(clientStub);
+ }
+
+ // Put should fail when configured as readonly
+ @Test
+ public void testPutObject() throws IOException, OS3Exception {
+ Response response = objectEndpoint.put(bucketName, keyName, CONTENT
+ .length(), 1, null, body);
+
+ Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(),
+ response.getStatus());
+ }
+
+ // Get should succeed
+ @Test
+ public void get() throws IOException, OS3Exception {
+ //WHEN
+ Response response = objectEndpoint.get(bucketName, "key1",
+ null, 0, null, body);
+
+ //THEN
+ OzoneInputStream ozoneInputStream =
+ clientStub.getObjectStore().getS3Bucket(bucketName)
+ .readKey("key1");
+ String keyContent =
+ IOUtils.toString(ozoneInputStream, UTF_8);
+
+ Assert.assertEquals(CONTENT, keyContent);
+ Assert.assertEquals("" + keyContent.length(),
+ response.getHeaderString("Content-Length"));
+
+ DateTimeFormatter.RFC_1123_DATE_TIME
+ .parse(response.getHeaderString("Last-Modified"));
+ }
+
+ // Copy is also treated as write
+ @Test
+ public void testCopyObject() throws IOException, OS3Exception {
+ // Put object in to source bucket
+ objectEndpoint.setHeaders(headers);
+
+ Response response = objectEndpoint.put(bucketName, keyName,
+ CONTENT.length(), 1, null, body);
+ Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(),
+ response.getStatus());
+ }
+
+ @Test
+ public void testDelete() throws IOException, OS3Exception {
+ //GIVEN
+ OzoneBucket bucket =
+ clientStub.getObjectStore().getS3Bucket("b1");
+
+ bucket.createKey("key1", 0).close();
+
+ //WHEN
+ Response response = objectEndpoint.delete("b1", "key1", null);
+
+ //THEN
+ Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(),
+ response.getStatus());
+ }
+
+ @Test
+ public void testBucketPut() throws IOException, OS3Exception {
+ Response response = bucketEndpoint.put(bucketName, null, null, null);
+ Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(),
+ response.getStatus());
+ }
+
+ @Test
+ public void listDir() throws OS3Exception, IOException {
+ Response response =
+ bucketEndpoint.get(bucketName, "/", null, null, 100,
+ "dir1", null, null, null, null, null);
+
+ Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ }
+
+ @Test
+ public void testInitiateMultipartUpload() throws Exception {
+ Response response = objectEndpoint.initializeMultipartUpload(bucketName,
+ keyName);
+
+ Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(),
+ response.getStatus());
+ }
+}
diff --git a/pom.xml b/pom.xml
index 1f387ee1f81..c496a61525d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,7 +66,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs
${ozone.version}
- 1.3.0
+ 1.3.2-SNAPSHOT
Grand Canyon
${hdds.version}
${ozone.version}