diff --git a/core/src/main/java/org/elasticsearch/repositories/Repository.java b/core/src/main/java/org/elasticsearch/repositories/Repository.java index 2bcdccb5565c0..adde0ed1c898c 100644 --- a/core/src/main/java/org/elasticsearch/repositories/Repository.java +++ b/core/src/main/java/org/elasticsearch/repositories/Repository.java @@ -130,4 +130,10 @@ public interface Repository extends LifecycleComponent { */ void endVerification(String verificationToken); + /** + * Returns true if the repository supports only read operations + * @return true if the repository is read/only + */ + boolean readOnly(); + } diff --git a/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 3e9fbaaad09c9..44ccb945d3d2e 100644 --- a/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -168,6 +168,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent snapshotLegacyFormat; + private final boolean readOnly; + /** * Constructs new BlobStoreRepository * @@ -181,6 +183,7 @@ protected BlobStoreRepository(String repositoryName, RepositorySettings reposito this.indexShardRepository = (BlobStoreIndexShardRepository) indexShardRepository; snapshotRateLimiter = getRateLimiter(repositorySettings, "max_snapshot_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB)); restoreRateLimiter = getRateLimiter(repositorySettings, "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB)); + readOnly = repositorySettings.settings().getAsBoolean("readonly", false); } /** @@ -260,6 +263,9 @@ protected ByteSizeValue chunkSize() { */ @Override public void initializeSnapshot(SnapshotId snapshotId, List indices, MetaData metaData) { + if (readOnly()) { + throw new RepositoryException(this.repositoryName, "cannot create snapshot in a readonly repository"); + } try { if (snapshotFormat.exists(snapshotsBlobContainer, snapshotId.getSnapshot()) || snapshotLegacyFormat.exists(snapshotsBlobContainer, snapshotId.getSnapshot())) { @@ -283,6 +289,9 @@ public void initializeSnapshot(SnapshotId snapshotId, List indices, Meta */ @Override public void deleteSnapshot(SnapshotId snapshotId) { + if (readOnly()) { + throw new RepositoryException(this.repositoryName, "cannot delete snapshot from a readonly repository"); + } List indices = Collections.EMPTY_LIST; Snapshot snapshot = null; try { @@ -622,16 +631,21 @@ public long restoreThrottleTimeInNanos() { @Override public String startVerification() { try { - String seed = Strings.randomBase64UUID(); - byte[] testBytes = Strings.toUTF8Bytes(seed); - BlobContainer testContainer = blobStore().blobContainer(basePath().add(testBlobPrefix(seed))); - String blobName = "master.dat"; - try (OutputStream outputStream = testContainer.createOutput(blobName + "-temp")) { - outputStream.write(testBytes); + if (readOnly()) { + // It's readonly - so there is not much we can do here to verify it + return null; + } else { + String seed = Strings.randomBase64UUID(); + byte[] testBytes = Strings.toUTF8Bytes(seed); + BlobContainer testContainer = blobStore().blobContainer(basePath().add(testBlobPrefix(seed))); + String blobName = "master.dat"; + try (OutputStream outputStream = testContainer.createOutput(blobName + "-temp")) { + outputStream.write(testBytes); + } + // Make sure that move is supported + testContainer.move(blobName + "-temp", blobName); + return seed; } - // Make sure that move is supported - testContainer.move(blobName + "-temp", blobName); - return seed; } catch (IOException exp) { throw new RepositoryVerificationException(repositoryName, "path " + basePath() + " is not accessible on master node", exp); } @@ -639,6 +653,9 @@ public String startVerification() { @Override public void endVerification(String seed) { + if (readOnly()) { + throw new UnsupportedOperationException("shouldn't be called"); + } try { blobStore().delete(basePath().add(testBlobPrefix(seed))); } catch (IOException exp) { @@ -649,4 +666,9 @@ public void endVerification(String seed) { public static String testBlobPrefix(String seed) { return TESTS_FILE + seed; } + + @Override + public boolean readOnly() { + return readOnly; + } } diff --git a/core/src/main/java/org/elasticsearch/repositories/uri/URLRepository.java b/core/src/main/java/org/elasticsearch/repositories/uri/URLRepository.java index 42a27c1dcd4e9..922c4878466ed 100644 --- a/core/src/main/java/org/elasticsearch/repositories/uri/URLRepository.java +++ b/core/src/main/java/org/elasticsearch/repositories/uri/URLRepository.java @@ -126,17 +126,6 @@ public List snapshots() { } } - @Override - public String startVerification() { - //TODO: #7831 Add check that URL exists and accessible - return null; - } - - @Override - public void endVerification(String seed) { - throw new UnsupportedOperationException("shouldn't be called"); - } - /** * Makes sure that the url is white listed or if it points to the local file system it matches one on of the root path in path.repo */ @@ -168,4 +157,9 @@ private URL checkURL(URL url) { throw new RepositoryException(repositoryName, "unsupported url protocol [" + protocol + "] from URL [" + url + "]"); } + @Override + public boolean readOnly() { + return true; + } + } diff --git a/core/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/core/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index 7e282305c89c8..1c42072b889eb 100644 --- a/core/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/core/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -64,6 +64,7 @@ import org.elasticsearch.index.store.IndexStore; import org.elasticsearch.indices.InvalidIndexNameException; import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.test.junit.annotations.TestLogging; import org.junit.Test; @@ -1317,6 +1318,63 @@ public void urlRepositoryTest() throws Exception { assertThat(getSnapshotsResponse.getSnapshots().size(), equalTo(0)); } + + @Test + public void readonlyRepositoryTest() throws Exception { + Client client = client(); + + logger.info("--> creating repository"); + Path repositoryLocation = randomRepoPath(); + assertAcked(client.admin().cluster().preparePutRepository("test-repo") + .setType("fs").setSettings(Settings.settingsBuilder() + .put("location", repositoryLocation) + .put("compress", randomBoolean()) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + + createIndex("test-idx"); + ensureGreen(); + + logger.info("--> indexing some data"); + for (int i = 0; i < 100; i++) { + index("test-idx", "doc", Integer.toString(i), "foo", "bar" + i); + } + refresh(); + + logger.info("--> snapshot"); + CreateSnapshotResponse createSnapshotResponse = client.admin().cluster().prepareCreateSnapshot("test-repo", "test-snap").setWaitForCompletion(true).setIndices("test-idx").get(); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), greaterThan(0)); + assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), equalTo(createSnapshotResponse.getSnapshotInfo().totalShards())); + + assertThat(client.admin().cluster().prepareGetSnapshots("test-repo").setSnapshots("test-snap").get().getSnapshots().get(0).state(), equalTo(SnapshotState.SUCCESS)); + + logger.info("--> delete index"); + cluster().wipeIndices("test-idx"); + + logger.info("--> create read-only URL repository"); + assertAcked(client.admin().cluster().preparePutRepository("readonly-repo") + .setType("fs").setSettings(Settings.settingsBuilder() + .put("location", repositoryLocation) + .put("compress", randomBoolean()) + .put("readonly", true) + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + logger.info("--> restore index after deletion"); + RestoreSnapshotResponse restoreSnapshotResponse = client.admin().cluster().prepareRestoreSnapshot("readonly-repo", "test-snap").setWaitForCompletion(true).setIndices("test-idx").execute().actionGet(); + assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0)); + + assertThat(client.prepareCount("test-idx").get().getCount(), equalTo(100L)); + + logger.info("--> list available shapshots"); + GetSnapshotsResponse getSnapshotsResponse = client.admin().cluster().prepareGetSnapshots("readonly-repo").get(); + assertThat(getSnapshotsResponse.getSnapshots(), notNullValue()); + assertThat(getSnapshotsResponse.getSnapshots().size(), equalTo(1)); + + logger.info("--> try deleting snapshot"); + assertThrows(client.admin().cluster().prepareDeleteSnapshot("readonly-repo", "test-snap"), RepositoryException.class, "cannot delete snapshot from a readonly repository"); + + logger.info("--> try making another snapshot"); + assertThrows(client.admin().cluster().prepareCreateSnapshot("readonly-repo", "test-snap-2").setWaitForCompletion(true).setIndices("test-idx"), RepositoryException.class, "cannot create snapshot in a readonly repository"); + } + @Test public void throttlingTest() throws Exception { Client client = client(); diff --git a/docs/plugins/cloud-aws.asciidoc b/docs/plugins/cloud-aws.asciidoc index 7b0fee374e31b..1cab14732563f 100644 --- a/docs/plugins/cloud-aws.asciidoc +++ b/docs/plugins/cloud-aws.asciidoc @@ -303,6 +303,9 @@ The following settings are supported: Number of retries in case of S3 errors. Defaults to `3`. +`read_only`:: + + Makes repository read-only. coming[2.1.0] Defaults to `false`. The S3 repositories use the same credentials as the rest of the AWS services provided by this plugin (`discovery`). See <> for details. diff --git a/docs/plugins/cloud-azure.asciidoc b/docs/plugins/cloud-azure.asciidoc index 80fd189ad2e2d..3d0e7d0cc39bd 100644 --- a/docs/plugins/cloud-azure.asciidoc +++ b/docs/plugins/cloud-azure.asciidoc @@ -545,6 +545,10 @@ The Azure repository supports following settings: setting doesn't affect index files that are already compressed by default. Defaults to `false`. +`read_only`:: + + Makes repository read-only. coming[2.1.0] Defaults to `false`. + Some examples, using scripts: [source,json] diff --git a/docs/reference/modules/snapshots.asciidoc b/docs/reference/modules/snapshots.asciidoc index 32f412e204ce6..90f2a026cb9c2 100644 --- a/docs/reference/modules/snapshots.asciidoc +++ b/docs/reference/modules/snapshots.asciidoc @@ -121,6 +121,7 @@ The following settings are supported: using size value notation, i.e. 1g, 10m, 5k. Defaults to `null` (unlimited chunk size). `max_restore_bytes_per_sec`:: Throttles per node restore rate. Defaults to `40mb` per second. `max_snapshot_bytes_per_sec`:: Throttles per node snapshot rate. Defaults to `40mb` per second. +`readonly`:: Makes repository read-only. coming[2.1.0] Defaults to `false`. [float] ===== Read-only URL Repository