From 4928aebea0480acbf04bf9155c23b10652567870 Mon Sep 17 00:00:00 2001 From: Stefan Miklosovic Date: Fri, 29 Jan 2021 15:41:37 +0100 Subject: [PATCH] hardened parsing of DatabaseEntities, validation moved to operation --- create-data.cql | 4 +- .../esop/impl/AbstractOperationRequest.java | 30 ++++- .../esop/impl/DatabaseEntities.java | 7 +- .../esop/impl/RenamedEntities.java | 2 +- .../esop/impl/StorageLocation.java | 73 +---------- .../esop/impl/_import/ImportOperation.java | 2 - .../impl/_import/ImportOperationRequest.java | 3 - .../BackupCommitLogsOperationRequest.java | 22 +++- .../esop/impl/backup/BackupOperation.java | 12 +- .../impl/backup/BackupOperationRequest.java | 45 ++++++- .../backup/BaseBackupOperationRequest.java | 2 - ...ValidBackupCommitLogsOperationRequest.java | 55 -------- .../backup/ValidBackupOperationRequest.java | 81 ------------ .../esop/impl/restore/RestoreOperation.java | 12 +- .../impl/restore/RestoreOperationRequest.java | 85 +++++++++++- .../restore/ValidRestoreOperationRequest.java | 123 ------------------ .../esop/impl/retry/RetrySpec.java | 23 +++- .../esop/backup/CassandraDataTest.java | 30 +++++ .../embedded/local/LocalBackupTest.java | 9 +- 19 files changed, 251 insertions(+), 369 deletions(-) delete mode 100644 src/main/java/com/instaclustr/esop/impl/backup/ValidBackupCommitLogsOperationRequest.java delete mode 100644 src/main/java/com/instaclustr/esop/impl/backup/ValidBackupOperationRequest.java delete mode 100644 src/main/java/com/instaclustr/esop/impl/restore/ValidRestoreOperationRequest.java diff --git a/create-data.cql b/create-data.cql index ded386c3..c1b22709 100644 --- a/create-data.cql +++ b/create-data.cql @@ -1,5 +1,5 @@ -CREATE KEYSPACE IF NOT EXISTS test1 WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3}; -CREATE KEYSPACE IF NOT EXISTS test2 WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3}; +CREATE KEYSPACE IF NOT EXISTS test1 WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 2}; +CREATE KEYSPACE IF NOT EXISTS test2 WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 2}; CREATE TABLE IF NOT EXISTS test1.testtable1 ( id uuid primary key); CREATE TABLE IF NOT EXISTS test1.testtable2 ( id uuid primary key); diff --git a/src/main/java/com/instaclustr/esop/impl/AbstractOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/AbstractOperationRequest.java index f1caf8f0..2e80c349 100644 --- a/src/main/java/com/instaclustr/esop/impl/AbstractOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/AbstractOperationRequest.java @@ -1,6 +1,9 @@ package com.instaclustr.esop.impl; -import javax.validation.constraints.NotNull; +import static java.lang.String.format; + +import java.util.Arrays; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -9,7 +12,6 @@ import com.instaclustr.esop.impl.StorageLocation.StorageLocationDeserializer; import com.instaclustr.esop.impl.StorageLocation.StorageLocationSerializer; import com.instaclustr.esop.impl.StorageLocation.StorageLocationTypeConverter; -import com.instaclustr.esop.impl.StorageLocation.ValidStorageLocation; import com.instaclustr.esop.impl.retry.RetrySpec; import com.instaclustr.kubernetes.KubernetesSecretsReader; import com.instaclustr.operations.OperationRequest; @@ -28,8 +30,6 @@ public abstract class AbstractOperationRequest extends OperationRequest { "cloudProvider://bucketName/clusterId/datacenterId/nodeId or file:///some/path/bucketName/clusterId/datacenterId/nodeId. " + "'cloudProvider' is one of 's3', 'oracle', 'azure' or 'gcp'.", required = true) - @NotNull - @ValidStorageLocation @JsonSerialize(using = StorageLocationSerializer.class) @JsonDeserialize(using = StorageLocationDeserializer.class) public StorageLocation storageLocation; @@ -68,7 +68,7 @@ public AbstractOperationRequest() { // for picocli } - public AbstractOperationRequest(@NotNull final StorageLocation storageLocation, + public AbstractOperationRequest(final StorageLocation storageLocation, final String k8sNamespace, final String k8sSecretName, final boolean insecure, @@ -113,4 +113,24 @@ public String resolveKubernetesNamespace() { return resolvedNamespace; } + + public void validate(final Set storageProviders) { + if (storageLocation == null) { + throw new IllegalStateException("storageLocation has to be specified!"); + } + + if (retry != null) { + retry.validate(); + } + + try { + storageLocation.validate(); + } catch (Exception ex) { + throw new IllegalStateException(format("Invalid storage location: %s", ex.getLocalizedMessage())); + } + + if (storageProviders != null && !storageProviders.contains(storageLocation.storageProvider)) { + throw new IllegalStateException(format("Available storage providers: %s", Arrays.toString(storageProviders.toArray()))); + } + } } diff --git a/src/main/java/com/instaclustr/esop/impl/DatabaseEntities.java b/src/main/java/com/instaclustr/esop/impl/DatabaseEntities.java index 92ff3f02..f7a9d9f5 100644 --- a/src/main/java/com/instaclustr/esop/impl/DatabaseEntities.java +++ b/src/main/java/com/instaclustr/esop/impl/DatabaseEntities.java @@ -8,6 +8,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.core.JsonGenerator; @@ -141,9 +143,8 @@ public static DatabaseEntities parse(final String entities) { return DatabaseEntities.empty(); } - final String sanitizedEntities = entities.replaceAll("[ ]+", ""); - - final String[] keyspaceTablePairs = sanitizedEntities.split(","); + final String sanitized = entities.trim().replaceAll("[ ]+\\.", ".").replaceAll("\\.[ ]+", ".").replaceAll(" ", ",").replaceAll("[,]+", ","); + final String[] keyspaceTablePairs = Stream.of(sanitized.split(",")).filter(s -> !s.isEmpty()).toArray(String[]::new); final List keyspaces = new ArrayList<>(); final Multimap keyspacesWithTables = HashMultimap.create(); diff --git a/src/main/java/com/instaclustr/esop/impl/RenamedEntities.java b/src/main/java/com/instaclustr/esop/impl/RenamedEntities.java index 60ed76e8..7d7417cb 100644 --- a/src/main/java/com/instaclustr/esop/impl/RenamedEntities.java +++ b/src/main/java/com/instaclustr/esop/impl/RenamedEntities.java @@ -38,7 +38,7 @@ public boolean areEmpty() { return this.renamed.isEmpty(); } - public static void validate(final Map rename) throws Exception { + public static void validate(final Map rename) { if (rename == null) { return; } diff --git a/src/main/java/com/instaclustr/esop/impl/StorageLocation.java b/src/main/java/com/instaclustr/esop/impl/StorageLocation.java index a0fc1718..c6a677e7 100644 --- a/src/main/java/com/instaclustr/esop/impl/StorageLocation.java +++ b/src/main/java/com/instaclustr/esop/impl/StorageLocation.java @@ -1,22 +1,10 @@ package com.instaclustr.esop.impl; import static java.lang.String.format; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; + import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,8 +17,6 @@ import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.google.common.base.MoreObjects; -import com.google.inject.Inject; -import com.instaclustr.esop.guice.StorageProviders; import picocli.CommandLine; import picocli.CommandLine.ITypeConverter; @@ -62,7 +48,7 @@ public StorageLocation(final String rawLocation) { initializeFileBackupLocation(this.rawLocation); } else { cloudLocation = true; - initializeCloudBackupLocation(this.rawLocation); + initializeCloudLocation(this.rawLocation); } } @@ -86,7 +72,7 @@ private void initializeFileBackupLocation(final String backupLocation) { } } - private void initializeCloudBackupLocation(final String storageLocation) { + private void initializeCloudLocation(final String storageLocation) { final Matcher globalMatcher = globalPattern.matcher(storageLocation); @@ -115,14 +101,14 @@ public void validate() throws IllegalStateException { if (cloudLocation) { if (!globalRequest) { if (rawLocation == null || storageProvider == null || bucket == null || clusterId == null || datacenterId == null || nodeId == null) { - throw new IllegalStateException(format("Backup location %s is not in form protocol://bucketName/clusterId/datacenterid/nodeId", + throw new IllegalStateException(format("Storage location %s is not in form protocol://bucketName/clusterId/datacenterid/nodeId", rawLocation)); } } else if (rawLocation == null || storageProvider == null || bucket == null) { throw new IllegalStateException(format("Global storage location %s is not in form protocol://bucketName", rawLocation)); } } else if (rawLocation == null || storageProvider == null || bucket == null || clusterId == null || datacenterId == null || nodeId == null || fileBackupDirectory == null) { - throw new IllegalStateException(format("Backup location %s is not in form file:///some/backup/path/clusterId/datacenterId/nodeId", + throw new IllegalStateException(format("Storage location %s is not in form file:///some/backup/path/clusterId/datacenterId/nodeId", rawLocation)); } @@ -192,55 +178,6 @@ public String toString() { .toString(); } - @Target({TYPE, PARAMETER, FIELD}) - @Retention(RUNTIME) - @Constraint(validatedBy = ValidStorageLocation.StorageLocationValidator.class) - public @interface ValidStorageLocation { - - String message() default "{com.instaclustr.esop.impl.StorageLocation.StorageLocationValidator.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - class StorageLocationValidator implements ConstraintValidator { - - private final Set storageProviders; - - @Inject - public StorageLocationValidator(final @StorageProviders Set storageProviders) { - this.storageProviders = storageProviders; - } - - @Override - public boolean isValid(final StorageLocation value, final ConstraintValidatorContext context) { - - if (value == null) { - return true; - } - - context.disableDefaultConstraintViolation(); - - try { - value.validate(); - } catch (Exception ex) { - context.buildConstraintViolationWithTemplate(format("Invalid backup location: %s", - ex.getLocalizedMessage())).addConstraintViolation(); - return false; - } - - if (!storageProviders.contains(value.storageProvider)) { - context.buildConstraintViolationWithTemplate(format("Available providers: %s", - Arrays.toString(storageProviders.toArray()))).addConstraintViolation(); - - return false; - } - - return true; - } - } - } - public static class StorageLocationTypeConverter implements ITypeConverter { @Override diff --git a/src/main/java/com/instaclustr/esop/impl/_import/ImportOperation.java b/src/main/java/com/instaclustr/esop/impl/_import/ImportOperation.java index c2e30d47..704b5d35 100644 --- a/src/main/java/com/instaclustr/esop/impl/_import/ImportOperation.java +++ b/src/main/java/com/instaclustr/esop/impl/_import/ImportOperation.java @@ -4,7 +4,6 @@ import static java.lang.String.format; import static java.nio.file.Files.exists; -import javax.validation.constraints.NotNull; import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -64,7 +63,6 @@ private ImportOperation(@JsonProperty("type") final String type, @JsonProperty("noInvalidateCaches") final boolean noInvalidateCaches, @JsonProperty("quick") final boolean quick, @JsonProperty("extendedVerify") final boolean extendedVerify, - @NotNull @JsonProperty("sourceDir") @JsonDeserialize(using = NioPathDeserializer.class) @JsonSerialize(using = NioPathSerializer.class) final Path sourceDir) { diff --git a/src/main/java/com/instaclustr/esop/impl/_import/ImportOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/_import/ImportOperationRequest.java index d70c9cc5..ec197312 100644 --- a/src/main/java/com/instaclustr/esop/impl/_import/ImportOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/_import/ImportOperationRequest.java @@ -1,6 +1,5 @@ package com.instaclustr.esop.impl._import; -import javax.validation.constraints.NotNull; import java.nio.file.Path; import java.nio.file.Paths; @@ -134,7 +133,6 @@ public class ImportOperationRequest extends OperationRequest { description = "upon import, run an extended verify, verifying all values in the new sstables") public boolean extendedVerify = false; - @NotNull @JsonDeserialize(using = NioPathDeserializer.class) @JsonSerialize(using = NioPathSerializer.class) @Option(names = {"--import-source-dir"}, @@ -176,7 +174,6 @@ public ImportOperationRequest(@JsonProperty("type") final String type, @JsonProperty("noInvalidateCaches") final boolean noInvalidateCaches, @JsonProperty("quick") final boolean quick, @JsonProperty("extendedVerify") final boolean extendedVerify, - @NotNull @JsonProperty("sourceDir") @JsonDeserialize(using = NioPathDeserializer.class) @JsonSerialize(using = NioPathSerializer.class) final Path sourceDir) { diff --git a/src/main/java/com/instaclustr/esop/impl/backup/BackupCommitLogsOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/backup/BackupCommitLogsOperationRequest.java index 7e832817..7d63cc04 100644 --- a/src/main/java/com/instaclustr/esop/impl/backup/BackupCommitLogsOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/backup/BackupCommitLogsOperationRequest.java @@ -1,9 +1,13 @@ package com.instaclustr.esop.impl.backup; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; import com.amazonaws.services.s3.model.MetadataDirective; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -13,12 +17,12 @@ import com.instaclustr.esop.impl.retry.RetrySpec; import com.instaclustr.jackson.PathDeserializer; import com.instaclustr.jackson.PathSerializer; +import com.instaclustr.kubernetes.KubernetesHelper; import com.instaclustr.measure.DataRate; import com.instaclustr.measure.Time; import com.instaclustr.picocli.typeconverter.PathTypeConverter; import picocli.CommandLine.Option; -@ValidBackupCommitLogsOperationRequest public class BackupCommitLogsOperationRequest extends BaseBackupOperationRequest { @Option(names = {"--cl-archive"}, @@ -103,4 +107,20 @@ public String toString() { .add("retry", retry) .toString(); } + + @JsonIgnore + public void validate(final Set storageProviders) { + super.validate(storageProviders); + if (this.cassandraDirectory == null || this.cassandraDirectory.toFile().getAbsolutePath().equals("/")) { + this.cassandraDirectory = Paths.get("/var/lib/cassandra"); + } + + if (!Files.exists(this.cassandraDirectory)) { + throw new IllegalStateException(String.format("cassandraDirectory %s does not exist", cassandraDirectory)); + } + + if (KubernetesHelper.isRunningInKubernetes() && this.resolveKubernetesSecretName() == null) { + throw new IllegalStateException("This code is running in Kubernetes but there is not 'k8sSecretName' field set on backup request!"); + } + } } diff --git a/src/main/java/com/instaclustr/esop/impl/backup/BackupOperation.java b/src/main/java/com/instaclustr/esop/impl/backup/BackupOperation.java index 090d9ab8..58d5b8c1 100644 --- a/src/main/java/com/instaclustr/esop/impl/backup/BackupOperation.java +++ b/src/main/java/com/instaclustr/esop/impl/backup/BackupOperation.java @@ -1,10 +1,10 @@ package com.instaclustr.esop.impl.backup; -import javax.validation.constraints.Min; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -15,6 +15,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import com.instaclustr.esop.guice.StorageProviders; import com.instaclustr.esop.impl.DatabaseEntities; import com.instaclustr.esop.impl.DatabaseEntities.DatabaseEntitiesDeserializer; import com.instaclustr.esop.impl.DatabaseEntities.DatabaseEntitiesSerializer; @@ -33,10 +34,12 @@ public class BackupOperation extends Operation implement private static final Logger logger = LoggerFactory.getLogger(BackupOperation.class); + private final Set storageProviders; private final OperationCoordinator coordinator; @AssistedInject public BackupOperation(Optional> coordinator, + @StorageProviders Set storageProviders, @Assisted final BackupOperationRequest request) { super(request); @@ -45,11 +48,13 @@ public BackupOperation(Optional> co } this.coordinator = coordinator.get(); + this.storageProviders = storageProviders; } public BackupOperation(final BackupOperationRequest request) { super(request); this.coordinator = null; + this.storageProviders = null; this.type = "backup"; } @@ -79,7 +84,7 @@ private BackupOperation(@JsonProperty("type") final String type, @JsonProperty("k8sSecretName") final String k8sBackupSecretName, @JsonProperty("globalRequest") final boolean globalRequest, @JsonProperty("dc") final String dc, - @JsonProperty("timeout") @Min(1) final Integer timeout, + @JsonProperty("timeout") final Integer timeout, @JsonProperty("insecure") final boolean insecure, @JsonProperty("createMissingBucket") final boolean createMissingBucket, @JsonProperty("skipBucketVerification") final boolean skipBucketVerification, @@ -109,6 +114,7 @@ private BackupOperation(@JsonProperty("type") final String type, proxySettings, retry)); coordinator = null; + storageProviders = null; } @Override @@ -119,6 +125,8 @@ protected Object clone() throws CloneNotSupportedException { @Override protected void run0() throws Exception { assert coordinator != null; + assert storageProviders != null; + request.validate(storageProviders); coordinator.coordinate(this); } } diff --git a/src/main/java/com/instaclustr/esop/impl/backup/BackupOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/backup/BackupOperationRequest.java index 155216b6..8da5f0ab 100644 --- a/src/main/java/com/instaclustr/esop/impl/backup/BackupOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/backup/BackupOperationRequest.java @@ -1,14 +1,19 @@ package com.instaclustr.esop.impl.backup; +import static com.instaclustr.kubernetes.KubernetesHelper.isRunningAsClient; +import static com.instaclustr.kubernetes.KubernetesHelper.isRunningInKubernetes; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import javax.validation.constraints.Min; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; import com.amazonaws.services.s3.model.MetadataDirective; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -24,7 +29,6 @@ import com.instaclustr.measure.Time; import picocli.CommandLine.Option; -@ValidBackupOperationRequest public class BackupOperationRequest extends BaseBackupOperationRequest { @Option(names = {"-s", "--st", "--snapshot-tag"}, @@ -88,7 +92,7 @@ public BackupOperationRequest(@JsonProperty("type") final String type, @JsonProperty("k8sSecretName") final String k8sSecretName, @JsonProperty("globalRequest") final boolean globalRequest, @JsonProperty("dc") final String dc, - @JsonProperty("timeout") @Min(1) final Integer timeout, + @JsonProperty("timeout") final Integer timeout, @JsonProperty("insecure") final boolean insecure, @JsonProperty("createMissingBucket") final boolean createMissingBucket, @JsonProperty("skipBucketVerification") final boolean skipBucketVerification, @@ -114,7 +118,7 @@ public BackupOperationRequest(@JsonProperty("type") final String type, this.globalRequest = globalRequest; this.type = type; this.dc = dc; - this.timeout = timeout == null ? 5 : timeout; + this.timeout = timeout == null || timeout < 1 ? 5 : timeout; this.schemaVersion = schemaVersion; this.uploadClusterTopology = uploadClusterTopology; } @@ -144,4 +148,37 @@ public String toString() { .add("retry", retry) .toString(); } + + @JsonIgnore + public void validate(final Set storageProviders) { + super.validate(storageProviders); + + if (this.cassandraDirectory == null || this.cassandraDirectory.toFile().getAbsolutePath().equals("/")) { + this.cassandraDirectory = Paths.get("/var/lib/cassandra"); + } + + if (!Files.exists(this.cassandraDirectory)) { + throw new IllegalStateException(String.format("cassandraDirectory %s does not exist", this.cassandraDirectory)); + } + + if ((isRunningInKubernetes() || isRunningAsClient())) { + if (this.resolveKubernetesSecretName() == null) { + throw new IllegalStateException("This code is running in Kubernetes or as a Kubernetes client but it is not possible to resolve k8s secret name for backups!"); + } + + if (this.resolveKubernetesNamespace() == null) { + throw new IllegalStateException("This code is running in Kubernetes or as a Kubernetes client but it is not possible to resolve k8s namespace for backups!"); + } + } + + if (this.entities == null) { + this.entities = DatabaseEntities.empty(); + } + + try { + DatabaseEntities.validateForRequest(this.entities); + } catch (final Exception ex) { + throw new IllegalStateException(ex.getMessage()); + } + } } diff --git a/src/main/java/com/instaclustr/esop/impl/backup/BaseBackupOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/backup/BaseBackupOperationRequest.java index e0a6e4e7..630177c6 100644 --- a/src/main/java/com/instaclustr/esop/impl/backup/BaseBackupOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/backup/BaseBackupOperationRequest.java @@ -1,6 +1,5 @@ package com.instaclustr.esop.impl.backup; -import javax.validation.constraints.NotNull; import java.nio.file.Path; import java.nio.file.Paths; @@ -29,7 +28,6 @@ public class BaseBackupOperationRequest extends AbstractOperationRequest { defaultValue = "/var/lib/cassandra") @JsonSerialize(using = PathSerializer.class) @JsonDeserialize(using = PathDeserializer.class) - @NotNull public Path cassandraDirectory; @Option(names = {"-d", "--duration"}, diff --git a/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupCommitLogsOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupCommitLogsOperationRequest.java deleted file mode 100644 index 4280e203..00000000 --- a/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupCommitLogsOperationRequest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.instaclustr.esop.impl.backup; - -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.file.Files; -import java.nio.file.Paths; - -import com.instaclustr.kubernetes.KubernetesHelper; - -@Target({TYPE, PARAMETER}) -@Retention(RUNTIME) -@Constraint(validatedBy = { - ValidBackupCommitLogsOperationRequest.BackupCommitLogsOperationRequestValidator.class, -}) -public @interface ValidBackupCommitLogsOperationRequest { - - String message() default "{com.instaclustr.esop.impl.backup.ValidBackupCommitLogsOperationRequest.BackupCommitLogsOperationRequestValidator.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - final class BackupCommitLogsOperationRequestValidator implements ConstraintValidator { - - @Override - public boolean isValid(final BackupCommitLogsOperationRequest value, final ConstraintValidatorContext context) { - - context.disableDefaultConstraintViolation(); - - if (value.cassandraDirectory == null || value.cassandraDirectory.toFile().getAbsolutePath().equals("/")) { - value.cassandraDirectory = Paths.get("/var/lib/cassandra"); - } - - if (!Files.exists(value.cassandraDirectory)) { - context.buildConstraintViolationWithTemplate(String.format("cassandraDirectory %s does not exist", value.cassandraDirectory)).addConstraintViolation(); - return false; - } - - if (KubernetesHelper.isRunningInKubernetes() && value.resolveKubernetesSecretName() == null) { - context.buildConstraintViolationWithTemplate("This code is running in Kubernetes but there is not 'k8sSecretName' field set on backup request!").addConstraintViolation(); - return false; - } - - return true; - } - } -} diff --git a/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupOperationRequest.java deleted file mode 100644 index 2da6eb4d..00000000 --- a/src/main/java/com/instaclustr/esop/impl/backup/ValidBackupOperationRequest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.instaclustr.esop.impl.backup; - -import static com.instaclustr.kubernetes.KubernetesHelper.isRunningAsClient; -import static com.instaclustr.kubernetes.KubernetesHelper.isRunningInKubernetes; -import static java.lang.String.format; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.file.Files; -import java.nio.file.Paths; - -import com.instaclustr.esop.impl.DatabaseEntities; - -@Target({TYPE, PARAMETER}) -@Retention(RUNTIME) -@Constraint(validatedBy = { - ValidBackupOperationRequest.BackupOperationRequestValidator.class, -}) -public @interface ValidBackupOperationRequest { - - String message() default "{com.instaclustr.esop.impl.backup.ValidBackupOperationRequest.BackupOperationRequestValidator.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - final class BackupOperationRequestValidator implements ConstraintValidator { - - @Override - public boolean isValid(final BackupOperationRequest value, final ConstraintValidatorContext context) { - - context.disableDefaultConstraintViolation(); - - if (value.cassandraDirectory == null || value.cassandraDirectory.toFile().getAbsolutePath().equals("/")) { - value.cassandraDirectory = Paths.get("/var/lib/cassandra"); - } - - if (!Files.exists(value.cassandraDirectory)) { - context.buildConstraintViolationWithTemplate(format("cassandraDirectory %s does not exist", value.cassandraDirectory)).addConstraintViolation(); - return false; - } - - if ((isRunningInKubernetes() || isRunningAsClient())) { - - if (value.resolveKubernetesSecretName() == null) { - context.buildConstraintViolationWithTemplate("This code is running in Kubernetes or as a Kubernetes client " - + "but it is not possible to resolve k8s secret name for backups!").addConstraintViolation(); - - return false; - } - - if (value.resolveKubernetesNamespace() == null) { - context.buildConstraintViolationWithTemplate("This code is running in Kubernetes or as a Kubernetes client " - + "but it is not possible to resolve k8s namespace for backups!").addConstraintViolation(); - - return false; - } - } - - if (value.entities == null) { - value.entities = DatabaseEntities.empty(); - } - - try { - DatabaseEntities.validateForRequest(value.entities); - } catch (final Exception ex) { - context.buildConstraintViolationWithTemplate(ex.getMessage()).addConstraintViolation(); - return false; - } - - return true; - } - } -} diff --git a/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperation.java b/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperation.java index 44e106f2..286ee7f5 100644 --- a/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperation.java +++ b/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperation.java @@ -1,11 +1,11 @@ package com.instaclustr.esop.impl.restore; -import javax.validation.constraints.Min; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ser.std.UUIDSerializer; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import com.instaclustr.esop.guice.StorageProviders; import com.instaclustr.esop.impl.DatabaseEntities; import com.instaclustr.esop.impl.ProxySettings; import com.instaclustr.esop.impl.StorageLocation; @@ -30,9 +31,11 @@ public class RestoreOperation extends Operation implements Cloneable { private final OperationCoordinator coordinator; + private final Set storageProviders; @AssistedInject public RestoreOperation(Optional> coordinator, + @StorageProviders Set storageProviders, @Assisted final RestoreOperationRequest request) { super(request); @@ -41,11 +44,13 @@ public RestoreOperation(Optional> } this.coordinator = coordinator.get(); + this.storageProviders = storageProviders; } public RestoreOperation(final RestoreOperationRequest request) { super(request); this.coordinator = null; + this.storageProviders = null; this.type = "restore"; } @@ -79,7 +84,7 @@ private RestoreOperation(@JsonProperty("type") final String type, @JsonProperty("k8sNamespace") final String k8sNamespace, @JsonProperty("k8sSecretName") final String k8sSecretName, @JsonProperty("globalRequest") final boolean globalRequest, - @JsonProperty("timeout") @Min(1) final Integer timeout, + @JsonProperty("timeout") final Integer timeout, @JsonProperty("resolveHostIdFromTopology") final Boolean resolveHostIdFromTopology, @JsonProperty("insecure") final boolean insecure, @JsonProperty("newCluster") final boolean newCluster, @@ -119,6 +124,7 @@ private RestoreOperation(@JsonProperty("type") final String type, retry, singlePhase)); this.coordinator = null; + this.storageProviders = null; } @Override @@ -129,6 +135,8 @@ protected Object clone() throws CloneNotSupportedException { @Override protected void run0() throws Exception { assert coordinator != null; + assert storageProviders != null; + request.validate(storageProviders); coordinator.coordinate(this); } } diff --git a/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperationRequest.java index 75c42480..d2aa7e98 100644 --- a/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperationRequest.java +++ b/src/main/java/com/instaclustr/esop/impl/restore/RestoreOperationRequest.java @@ -1,12 +1,18 @@ package com.instaclustr.esop.impl.restore; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; +import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.HARDLINKS; +import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.IMPORT; +import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.IN_PLACE; +import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.UNKNOWN; +import static java.lang.String.format; + +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; @@ -23,6 +29,7 @@ import com.instaclustr.esop.impl.DatabaseEntities.DatabaseEntitiesSerializer; import com.instaclustr.esop.impl.Directories; import com.instaclustr.esop.impl.ProxySettings; +import com.instaclustr.esop.impl.RenamedEntities; import com.instaclustr.esop.impl.StorageLocation; import com.instaclustr.esop.impl._import.ImportOperationRequest; import com.instaclustr.esop.impl.restore.RestorationPhase.RestorationPhaseType; @@ -32,10 +39,10 @@ import com.instaclustr.esop.impl.retry.RetrySpec; import com.instaclustr.jackson.PathDeserializer; import com.instaclustr.jackson.PathSerializer; +import com.instaclustr.kubernetes.KubernetesHelper; import com.instaclustr.picocli.typeconverter.PathTypeConverter; import picocli.CommandLine.Option; -@ValidRestoreOperationRequest public class RestoreOperationRequest extends BaseRestoreOperationRequest { @JsonIgnore @@ -64,7 +71,6 @@ public class RestoreOperationRequest extends BaseRestoreOperationRequest { @Option(names = {"-s", "--st", "--snapshot-tag"}, description = "Snapshot to download and restore.", required = true) - @NotBlank public String snapshotTag; @Option(names = {"--entities"}, @@ -183,7 +189,7 @@ public RestoreOperationRequest(@JsonProperty("type") final String type, @JsonProperty("k8sNamespace") final String k8sNamespace, @JsonProperty("k8sSecretName") final String k8sSecretName, @JsonProperty("globalRequest") final boolean globalRequest, - @JsonProperty("timeout") @Min(1) final Integer timeout, + @JsonProperty("timeout") final Integer timeout, @JsonProperty("resolveHostIdFromTopology") final boolean resolveHostIdFromTopology, @JsonProperty("insecure") final boolean insecure, @JsonProperty("newCluster") final boolean newCluster, @@ -209,7 +215,7 @@ public RestoreOperationRequest(@JsonProperty("type") final String type, this.exactSchemaVersion = exactSchemaVersion; this.globalRequest = globalRequest; this.type = type; - this.timeout = timeout == null ? 5 : timeout; + this.timeout = timeout == null || timeout < 1 ? 5 : timeout; this.resolveHostIdFromTopology = resolveHostIdFromTopology; this.newCluster = newCluster; this.rename = rename == null ? Collections.emptyMap() : rename; @@ -249,4 +255,71 @@ public String toString() { .add("singlePhase", singlePhase) .toString(); } + + @JsonIgnore + public void validate(Set storageProviders) { + super.validate(storageProviders); + + if (this.snapshotTag == null || this.snapshotTag.isEmpty()) { + throw new IllegalStateException("snapshotTag can not be blank!"); + } + + if (this.restorationPhase == null) { + this.restorationPhase = RestorationPhaseType.UNKNOWN; + } + + if (this.restorationStrategyType == UNKNOWN) { + throw new IllegalStateException("restorationStrategyType is not recognized"); + } + + if (this.restorationStrategyType != IN_PLACE) { + if (this.restorationPhase == RestorationPhaseType.UNKNOWN) { + throw new IllegalStateException("restorationPhase is not recognized, it has to be set when you use IMPORT or HARDLINKS strategy type"); + } + } + + if (this.restorationStrategyType == IN_PLACE && this.restorationPhase != RestorationPhaseType.UNKNOWN) { + throw new IllegalStateException(format("you can not set restorationPhase %s when your restorationStrategyType is IN_PLACE", this.restorationPhase)); + } + + if (this.restorationStrategyType == IMPORT || this.restorationStrategyType == HARDLINKS) { + if (this.importing == null) { + throw new IllegalStateException(format("you can not specify %s restorationStrategyType and have 'import' field empty!", this.restorationStrategyType)); + } + } + + if (!Files.exists(this.cassandraDirectory)) { + throw new IllegalStateException(format("cassandraDirectory %s does not exist", this.cassandraDirectory)); + } + + if ((KubernetesHelper.isRunningInKubernetes() || KubernetesHelper.isRunningAsClient())) { + if (this.resolveKubernetesSecretName() == null) { + throw new IllegalStateException("This code is running in Kubernetes or as a Kubernetes client but it is not possible to resolve k8s secret name for restores!"); + } + + if (this.resolveKubernetesNamespace() == null) { + throw new IllegalStateException("This code is running in Kubernetes or as a Kubernetes client but it is not possible to resolve k8s namespace for restores!"); + } + } + + if (this.entities == null) { + this.entities = DatabaseEntities.empty(); + } + + try { + DatabaseEntities.validateForRequest(this.entities); + } catch (final Exception ex) { + throw new IllegalStateException(ex.getMessage()); + } + + try { + RenamedEntities.validate(this.rename); + } catch (final Exception ex) { + throw new IllegalStateException("Invalid 'rename' parameter: " + ex.getMessage()); + } + + if (this.rename != null && !this.rename.isEmpty() && this.restorationStrategyType == IN_PLACE) { + throw new IllegalStateException("rename field can not be used for in-place strategy, only for import or hardlinks"); + } + } } diff --git a/src/main/java/com/instaclustr/esop/impl/restore/ValidRestoreOperationRequest.java b/src/main/java/com/instaclustr/esop/impl/restore/ValidRestoreOperationRequest.java deleted file mode 100644 index b5dbb125..00000000 --- a/src/main/java/com/instaclustr/esop/impl/restore/ValidRestoreOperationRequest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.instaclustr.esop.impl.restore; - -import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.HARDLINKS; -import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.IMPORT; -import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.IN_PLACE; -import static com.instaclustr.esop.impl.restore.RestorationStrategy.RestorationStrategyType.UNKNOWN; -import static java.lang.String.format; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.file.Files; - -import com.instaclustr.esop.impl.DatabaseEntities; -import com.instaclustr.esop.impl.RenamedEntities; -import com.instaclustr.esop.impl.restore.RestorationPhase.RestorationPhaseType; -import com.instaclustr.kubernetes.KubernetesHelper; - -@Target({TYPE, PARAMETER}) -@Retention(RUNTIME) -@Constraint(validatedBy = { - ValidRestoreOperationRequest.RestoreOperationRequestValidator.class, -}) -public @interface ValidRestoreOperationRequest { - - String message() default "{com.instaclustr.esop.impl.restore.ValidRestoreOperationRequest.RestoreOperationRequestValidator.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - final class RestoreOperationRequestValidator implements ConstraintValidator { - - @Override - public boolean isValid(final RestoreOperationRequest value, final ConstraintValidatorContext context) { - - context.disableDefaultConstraintViolation(); - - if (value.restorationPhase == null) { - value.restorationPhase = RestorationPhaseType.UNKNOWN; - } - - if (value.restorationStrategyType == UNKNOWN) { - context.buildConstraintViolationWithTemplate("restorationStrategyType is not recognized").addConstraintViolation(); - return false; - } - - if (value.restorationStrategyType != IN_PLACE) { - if (value.restorationPhase == RestorationPhaseType.UNKNOWN) { - context.buildConstraintViolationWithTemplate("restorationPhase is not recognized, it has to be set when you use IMPORT or HARDLINKS strategy type").addConstraintViolation(); - return false; - } - } - - if (value.restorationStrategyType == IN_PLACE && value.restorationPhase != RestorationPhaseType.UNKNOWN) { - context.buildConstraintViolationWithTemplate(format("you can not set restorationPhase %s when your restorationStrategyType is IN_PLACE", - value.restorationPhase)).addConstraintViolation(); - return false; - } - - if (value.restorationStrategyType == IMPORT || value.restorationStrategyType == HARDLINKS) { - if (value.importing == null) { - context.buildConstraintViolationWithTemplate(format("you can not specify %s restorationStrategyType and have 'import' field empty!", - value.restorationStrategyType)); - return false; - } - } - - if (!Files.exists(value.cassandraDirectory)) { - context.buildConstraintViolationWithTemplate(format("cassandraDirectory %s does not exist", value.cassandraDirectory)).addConstraintViolation(); - return false; - } - - if ((KubernetesHelper.isRunningInKubernetes() || KubernetesHelper.isRunningAsClient())) { - - if (value.resolveKubernetesSecretName() == null) { - context.buildConstraintViolationWithTemplate("This code is running in Kubernetes or as a Kubernetes client " - + "but it is not possible to resolve k8s secret name for restores!").addConstraintViolation(); - - return false; - } - - if (value.resolveKubernetesNamespace() == null) { - context.buildConstraintViolationWithTemplate("This code is running in Kubernetes or as a Kubernetes client " - + "but it is not possible to resolve k8s namespace for restores!").addConstraintViolation(); - - return false; - } - } - - if (value.entities == null) { - value.entities = DatabaseEntities.empty(); - } - - try { - DatabaseEntities.validateForRequest(value.entities); - } catch (final Exception ex) { - context.buildConstraintViolationWithTemplate(ex.getMessage()).addConstraintViolation(); - return false; - } - - try { - RenamedEntities.validate(value.rename); - } catch (final Exception ex) { - context.buildConstraintViolationWithTemplate("Invalid 'rename' parameter: " + ex.getMessage()).addConstraintViolation(); - return false; - } - - if (value.rename != null && !value.rename.isEmpty() && value.restorationStrategyType == IN_PLACE) { - context.buildConstraintViolationWithTemplate("rename field can not be used for in-place strategy, only for import or hardlinks").addConstraintViolation(); - return false; - } - - return true; - } - } -} diff --git a/src/main/java/com/instaclustr/esop/impl/retry/RetrySpec.java b/src/main/java/com/instaclustr/esop/impl/retry/RetrySpec.java index 36751c79..4825f344 100644 --- a/src/main/java/com/instaclustr/esop/impl/retry/RetrySpec.java +++ b/src/main/java/com/instaclustr/esop/impl/retry/RetrySpec.java @@ -2,7 +2,6 @@ import static java.lang.String.format; -import javax.validation.constraints.Min; import java.util.Arrays; import com.fasterxml.jackson.annotation.JsonCreator; @@ -16,7 +15,6 @@ public class RetrySpec { - @Min(1) @Option(names = "--retry-interval", defaultValue = "10", description = "interval between retries when downloading of SSTable file fails, in seconds, defalts to 10") @@ -28,7 +26,6 @@ public class RetrySpec { converter = RetryStrategyConverter.class) public RetryStrategy strategy; - @Min(1) @Option(names = "--retry-max-attempts", defaultValue = "3", description = "number of attempts to download SSTable file, defaults to 3") @@ -39,14 +36,14 @@ public class RetrySpec { public boolean enabled; @JsonCreator - public RetrySpec(@JsonProperty("interval") @Min(1) final int interval, + public RetrySpec(@JsonProperty("interval") final Integer interval, @JsonProperty("strategy") final RetryStrategy strategy, - @JsonProperty("maxAttempts") @Min(1) final int maxAttempts, + @JsonProperty("maxAttempts") final Integer maxAttempts, @JsonProperty("enabled") final boolean enabled) { - this.interval = interval; + this.interval = interval == null || interval < 1 ? 10 : interval; this.strategy = strategy == null ? RetryStrategy.LINEAR : strategy; this.enabled = enabled; - this.maxAttempts = maxAttempts; + this.maxAttempts = maxAttempts == null || maxAttempts < 1 ? 3 : maxAttempts; } public RetrySpec() { @@ -56,6 +53,18 @@ public RetrySpec() { this.enabled = false; } + public void validate() { + if (strategy == null) { + strategy = RetryStrategy.LINEAR; + } + if (interval < 1) { + interval = 10; + } + if (maxAttempts < 1) { + maxAttempts = 3; + } + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/src/test/java/com/instaclustr/esop/backup/CassandraDataTest.java b/src/test/java/com/instaclustr/esop/backup/CassandraDataTest.java index ba25babf..7aa1bbe4 100644 --- a/src/test/java/com/instaclustr/esop/backup/CassandraDataTest.java +++ b/src/test/java/com/instaclustr/esop/backup/CassandraDataTest.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -136,6 +138,34 @@ public void testRenamedEntitiesWithDatabaseEntities() { }}); } + @Test + public void testDatabaseEntitiesOnSpacesOnly() throws Exception { + + for (String entity : Stream.of(" ks1 .tb1 ks3. tb3 ", + "ks1.tb1 ks3.tb3", + " ks1 .tb1 ks3.tb3 ", + " ks1.tb1 ks3.tb3, ", + " ks1.tb1,ks3.tb3", + ",,,ks1.tb1,,,ks3.tb3,,,").collect(Collectors.toList())) { + DatabaseEntities parsed = DatabaseEntities.parse(entity); + + Assert.assertTrue(parsed.getKeyspacesAndTables().containsEntry("ks1", "tb1"), entity); + Assert.assertTrue(parsed.getKeyspacesAndTables().containsEntry("ks3", "tb3"), entity); + Assert.assertEquals(parsed.getKeyspacesAndTables().size(), 2, entity); + } + + for (String entity : Stream.of(" ks1 ks3 ", + "ks1 ks3", + " ks1,ks3, ,", + ",,,ks1,ks3,,,").collect(Collectors.toList())) { + DatabaseEntities parsed = DatabaseEntities.parse(entity); + + Assert.assertTrue(parsed.getKeyspaces().contains("ks1"), entity); + Assert.assertTrue(parsed.getKeyspaces().contains("ks3"), entity); + Assert.assertEquals(parsed.getKeyspaces().size(), 2, entity); + } + } + @Test public void testDatabaseEntities() throws Exception { CassandraData parsed = new CassandraData(tableIdsMap, paths); diff --git a/src/test/java/com/instaclustr/esop/backup/embedded/local/LocalBackupTest.java b/src/test/java/com/instaclustr/esop/backup/embedded/local/LocalBackupTest.java index 7a01644f..9762b86a 100644 --- a/src/test/java/com/instaclustr/esop/backup/embedded/local/LocalBackupTest.java +++ b/src/test/java/com/instaclustr/esop/backup/embedded/local/LocalBackupTest.java @@ -15,9 +15,12 @@ import java.util.List; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.datastax.oss.driver.api.core.CqlSession; import com.github.nosan.embedded.cassandra.api.Cassandra; @@ -240,8 +243,10 @@ public UploadUnit constructUnitToSubmit(final Backuper backuper, assert snapshot.isPresent(); assert snapshot2.isPresent(); - final BackupOperation backupOperation = new BackupOperation(operationCoordinator, backupOperationRequest); - final BackupOperation backupOperation2 = new BackupOperation(operationCoordinator, backupOperationRequest2); + Set providers = Stream.of("file").collect(Collectors.toSet()); + + final BackupOperation backupOperation = new BackupOperation(operationCoordinator, providers, backupOperationRequest); + final BackupOperation backupOperation2 = new BackupOperation(operationCoordinator, providers, backupOperationRequest2); final List manifestEntries = Manifest.from(snapshot.get()).getManifestEntries(); final List manifestEntries2 = Manifest.from(snapshot2.get()).getManifestEntries();