diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2Controller.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2Controller.java index 0d22139f..df48c199 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2Controller.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2Controller.java @@ -241,7 +241,7 @@ public Response uploadMetadata( @POST public Response restoreBackup(@Parameter(description = "Unique identifier of the backup", required = true) @PathParam("backupName") @NotBlank String backupName, - @RequestBody(description = "Restore request", required = true) + @RequestBody(description = "Restore request") @Valid RestoreRequest restoreRequest, @QueryParam("dryRun") @DefaultValue("false") boolean dryRun) { RestoreResponse response = dbBackupV2Service.restore(backupName, restoreRequest, dryRun); @@ -308,8 +308,6 @@ public Response getRestoreStatus(@Parameter(description = "Unique identifier of @Operation(summary = "Retry restore", description = "Retry a failed restore operation") @APIResponses({ - @APIResponse(responseCode = "200", description = "Restore operation retried successfully", - content = @Content(schema = @Schema(implementation = RestoreResponse.class))), @APIResponse(responseCode = "202", description = "Restore retry accepted and is being processed", content = @Content(schema = @Schema(implementation = RestoreResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @@ -324,7 +322,6 @@ public Response getRestoreStatus(@Parameter(description = "Unique identifier of public Response retryRestore(@Parameter(description = "Unique identifier of the restore operation", required = true) @PathParam("restoreName") String restoreName) { - dbBackupV2Service.retryRestore(restoreName); - return Response.ok().build(); + return Response.accepted(dbBackupV2Service.retryRestore(restoreName)).build(); } } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/converter/ClassifierConverter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/converter/ClassifierConverter.java index db48c193..a164b095 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/converter/ClassifierConverter.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/converter/ClassifierConverter.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; - import jakarta.persistence.AttributeConverter; + import java.io.IOException; import java.util.SortedMap; @@ -24,7 +24,8 @@ public String convertToDatabaseColumn(SortedMap attribute) { @Override public SortedMap convertToEntityAttribute(String dbData) { try { - return objectMapper.readValue(dbData, new TypeReference>() {}); + return objectMapper.readValue(dbData, new TypeReference>() { + }); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/DatabaseRegistryDbaasRepositoryImpl.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/DatabaseRegistryDbaasRepositoryImpl.java index 5cc6b2e8..60a6777d 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/DatabaseRegistryDbaasRepositoryImpl.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/DatabaseRegistryDbaasRepositoryImpl.java @@ -1,5 +1,6 @@ package com.netcracker.cloud.dbaas.dao.jpa; +import com.netcracker.cloud.dbaas.dto.backupV2.Filter; import com.netcracker.cloud.dbaas.entity.pg.Database; import com.netcracker.cloud.dbaas.entity.pg.DatabaseRegistry; import com.netcracker.cloud.dbaas.repositories.dbaas.DatabaseRegistryDbaasRepository; @@ -12,23 +13,18 @@ import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; - import net.jodah.failsafe.Failsafe; import net.jodah.failsafe.RetryPolicy; import org.apache.commons.lang.StringUtils; +import javax.annotation.Nullable; import java.time.Duration; import java.util.*; import java.util.concurrent.Callable; import java.util.function.Function; import java.util.stream.Collectors; -import javax.annotation.Nullable; - -import static com.netcracker.cloud.dbaas.Constants.MICROSERVICE_NAME; -import static com.netcracker.cloud.dbaas.Constants.ROLE; -import static com.netcracker.cloud.dbaas.Constants.SCOPE; -import static com.netcracker.cloud.dbaas.Constants.SCOPE_VALUE_TENANT; +import static com.netcracker.cloud.dbaas.Constants.*; import static com.netcracker.cloud.dbaas.config.ServicesConfig.DBAAS_REPOSITORIES_MUTEX; import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW; @@ -141,6 +137,11 @@ public List findAllTransactionalDatabaseRegistries(String name return databaseRegistryRepository.findAllByNamespaceAndDatabase_BgVersionNull(namespace); } + @Override + public List findAllDatabasesByFilter(List filters) { + return databaseRegistryRepository.findAllDatabasesByFilter(filters); + } + @Override public void delete(DatabaseRegistry databaseRegistry) { log.debug("Delete logical database with classifier {} and type {}", databaseRegistry.getClassifier(), databaseRegistry.getType()); diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupRequest.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupRequest.java index d825fb4b..ec6ec712 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupRequest.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupRequest.java @@ -1,7 +1,7 @@ package com.netcracker.cloud.dbaas.dto.backupV2; import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy; -import com.netcracker.cloud.dbaas.utils.validation.BackupGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupResponse.java index b555dfbf..448a35cd 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupResponse.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.netcracker.cloud.dbaas.enums.BackupStatus; import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy; -import com.netcracker.cloud.dbaas.utils.validation.BackupGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierResponse.java new file mode 100644 index 00000000..e965aa34 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierResponse.java @@ -0,0 +1,17 @@ +package com.netcracker.cloud.dbaas.dto.backupV2; + +import com.netcracker.cloud.dbaas.enums.ClassifierType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.SortedMap; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ClassifierResponse { + private ClassifierType type; + private SortedMap classifier; + private SortedMap classifierBeforeMapper; +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/Filter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/Filter.java index 51eaf204..91811f18 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/Filter.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/Filter.java @@ -1,5 +1,8 @@ package com.netcracker.cloud.dbaas.dto.backupV2; +import com.netcracker.cloud.dbaas.utils.validation.NotEmptyFilter; +import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.RestoreGroup; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; @@ -9,6 +12,7 @@ import java.util.List; @Data +@NotEmptyFilter(groups = {BackupGroup.class, RestoreGroup.class}) @NoArgsConstructor @Schema(description = "Single filter criteria for backup and restore operations") public class Filter { diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/FilterCriteria.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/FilterCriteria.java index e963d383..70144f08 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/FilterCriteria.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/FilterCriteria.java @@ -1,12 +1,14 @@ package com.netcracker.cloud.dbaas.dto.backupV2; -import com.netcracker.cloud.dbaas.utils.validation.BackupGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import java.util.ArrayList; import java.util.List; @Data @@ -23,13 +25,11 @@ public class FilterCriteria { ) @NotNull(groups = {BackupGroup.class}) @Size(min = 1, groups = {BackupGroup.class}) - private List filter; - @Schema( - description = "Include databases that match any of the filters in the list" - ) - private List include; + @Valid + private List filter = new ArrayList<>(); + @Schema( description = "Exclude databases that match any of the filters in the list" ) - private List exclude; + private List exclude = new ArrayList<>(); } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreDatabaseResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreDatabaseResponse.java index c054648c..d3f0669f 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreDatabaseResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreDatabaseResponse.java @@ -26,7 +26,7 @@ public class RestoreDatabaseResponse { description = "List of database classifiers. Each classifier is a sorted map of attributes.", examples = "[{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}]" ) - private List> classifiers; + private List classifiers; @Schema( description = "List of database users", examples = "[{\"name\":\"username\",\"role\":\"admin\"}" @@ -58,7 +58,7 @@ public class RestoreDatabaseResponse { required = true ) private String path; - @Schema(description = "Error message if the backup failed", examples = "Restore Not Found") + @Schema(description = "Error message if the backup failed", examples = "Restore Not Found") private String errorMessage; @Schema(description = "Timestamp when the restore was created", examples = "2025-11-13T12:34:56Z") private Instant creationTime; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreExternalDatabaseResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreExternalDatabaseResponse.java index a06ba0e4..112480be 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreExternalDatabaseResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreExternalDatabaseResponse.java @@ -5,7 +5,6 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.List; -import java.util.SortedMap; @Data @NoArgsConstructor @@ -19,6 +18,6 @@ public class RestoreExternalDatabaseResponse { description = "List of database classifiers. Each classifier is a sorted map of attributes.", examples = "[{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}]" ) - private List> classifiers; + private List classifiers; } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreRequest.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreRequest.java index d44f4408..1c761bae 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreRequest.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreRequest.java @@ -1,8 +1,9 @@ package com.netcracker.cloud.dbaas.dto.backupV2; import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy; -import com.netcracker.cloud.dbaas.utils.validation.RestoreGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.RestoreGroup; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.groups.ConvertGroup; import jakarta.validation.groups.Default; @@ -21,6 +22,7 @@ public class RestoreRequest { "restore-before-prod-update-20251203T1020-4t6S" } ) + @NotBlank private String restoreName; @Schema( description = "Name of the storage backend containing the restore", @@ -29,6 +31,7 @@ public class RestoreRequest { "s3-backend" } ) + @NotBlank private String storageName; @Schema( description = "Path to the restore file in the storage", @@ -37,6 +40,7 @@ public class RestoreRequest { "/backups" } ) + @NotBlank private String blobPath; @Schema( description = "Filter criteria", diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreResponse.java index 2c656959..9904b20c 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/RestoreResponse.java @@ -2,7 +2,7 @@ import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy; import com.netcracker.cloud.dbaas.enums.RestoreStatus; -import com.netcracker.cloud.dbaas.utils.validation.RestoreGroup; +import com.netcracker.cloud.dbaas.utils.validation.group.RestoreGroup; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.groups.ConvertGroup; @@ -89,10 +89,6 @@ public class RestoreResponse { examples = "Backup Not Found" ) private String errorMessage; - @Schema(description = "Aggregated duration of databases", examples = "1200") - private Long duration; - @Schema(description = "Total number of adapter requests", examples = "1") - private Integer attemptCount; @Schema( description = "List of logical restores", implementation = LogicalRestoreResponse.class, diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupDatabaseDelegate.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupDatabaseDelegate.java index a30e6453..7bd56f81 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupDatabaseDelegate.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupDatabaseDelegate.java @@ -1,10 +1,10 @@ package com.netcracker.cloud.dbaas.entity.dto.backupV2; +import com.netcracker.cloud.dbaas.entity.pg.backupV2.Classifier; import com.netcracker.cloud.dbaas.entity.pg.backupV2.BackupDatabase; import java.util.List; -import java.util.SortedMap; public record BackupDatabaseDelegate(BackupDatabase backupDatabase, - List> classifiers) {} + List classifiers) {} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupExternalDelegate.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupExternalDelegate.java new file mode 100644 index 00000000..34849b3e --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupExternalDelegate.java @@ -0,0 +1,9 @@ +package com.netcracker.cloud.dbaas.entity.dto.backupV2; + +import com.netcracker.cloud.dbaas.entity.pg.backupV2.Classifier; +import com.netcracker.cloud.dbaas.entity.pg.backupV2.BackupExternalDatabase; + +import java.util.List; + +public record BackupExternalDelegate(BackupExternalDatabase backupExternalDatabase, List classifiers) { +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Classifier.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Classifier.java new file mode 100644 index 00000000..b770bbc6 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Classifier.java @@ -0,0 +1,29 @@ +package com.netcracker.cloud.dbaas.entity.pg.backupV2; + +import com.netcracker.cloud.dbaas.enums.ClassifierType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.SortedMap; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Classifier { + private ClassifierType type; + private SortedMap classifier; + private SortedMap classifierBeforeMapper; + + @Override + public boolean equals(Object o) { + if (!(o instanceof Classifier that)) return false; + return type == that.type && Objects.equals(classifier, that.classifier) && Objects.equals(classifierBeforeMapper, that.classifierBeforeMapper); + } + + @Override + public int hashCode() { + return Objects.hash(type, classifier, classifierBeforeMapper); + } +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreDatabase.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreDatabase.java index ab5356d8..1a35a75c 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreDatabase.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreDatabase.java @@ -13,7 +13,6 @@ import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.SortedMap; import java.util.UUID; @Data @@ -41,7 +40,7 @@ public class RestoreDatabase { @NotNull @JdbcTypeCode(SqlTypes.JSON) @Column(columnDefinition = "jsonb") - private List> classifiers; + private List classifiers; @JdbcTypeCode(SqlTypes.JSON) @Column(columnDefinition = "jsonb") diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreExternalDatabase.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreExternalDatabase.java index 2295d3ca..d3327d21 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreExternalDatabase.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/RestoreExternalDatabase.java @@ -11,7 +11,6 @@ import org.hibernate.type.SqlTypes; import java.util.List; -import java.util.SortedMap; import java.util.UUID; @Data @@ -40,5 +39,5 @@ public class RestoreExternalDatabase { @NotNull @JdbcTypeCode(SqlTypes.JSON) @Column(columnDefinition = "jsonb") - private List> classifiers; + private List classifiers; } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/ClassifierType.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/ClassifierType.java new file mode 100644 index 00000000..770fc0ea --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/ClassifierType.java @@ -0,0 +1,5 @@ +package com.netcracker.cloud.dbaas.enums; + +public enum ClassifierType { + NEW, REPLACED, TRANSIENT_REPLACED +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/mapper/BackupV2Mapper.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/mapper/BackupV2Mapper.java index a6375838..08ce7e74 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/mapper/BackupV2Mapper.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/mapper/BackupV2Mapper.java @@ -2,6 +2,7 @@ import com.netcracker.cloud.dbaas.dto.Source; import com.netcracker.cloud.dbaas.dto.backupV2.*; +import com.netcracker.cloud.dbaas.entity.dto.backupV2.BackupExternalDelegate; import com.netcracker.cloud.dbaas.entity.pg.backupV2.*; import com.netcracker.cloud.dbaas.enums.BackupTaskStatus; import com.netcracker.cloud.dbaas.enums.RestoreTaskStatus; @@ -100,7 +101,13 @@ private static , R extends Enum> R mapStatus( } @Mapping(target = "id", ignore = true) - RestoreExternalDatabase toRestoreExternalDatabase(BackupExternalDatabase backupExternalDatabase); + @Mapping(target = "name", source = "backupExternalDatabase.name") + @Mapping(target = "type", source = "backupExternalDatabase.type") + @Mapping(target = "classifiers", source = "classifiers") + RestoreExternalDatabase toRestoreExternalDatabase(BackupExternalDelegate backupExternalDelegate); - List toRestoreExternalDatabases(List backupExternalDatabases); + List toRestoreExternalDatabases(List backupExternalDelegates); + + ClassifierResponse toClassifierResponse(Classifier classifier); + List toClassifierResponse(List classifiers); } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/DatabaseRegistryDbaasRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/DatabaseRegistryDbaasRepository.java index 1978fc78..190c6f5c 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/DatabaseRegistryDbaasRepository.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/DatabaseRegistryDbaasRepository.java @@ -1,15 +1,15 @@ package com.netcracker.cloud.dbaas.repositories.dbaas; +import com.netcracker.cloud.dbaas.dto.backupV2.Filter; import com.netcracker.cloud.dbaas.entity.pg.Database; import com.netcracker.cloud.dbaas.entity.pg.DatabaseRegistry; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; -import javax.annotation.Nullable; - public interface DatabaseRegistryDbaasRepository { List findAnyLogDbRegistryTypeByNamespace(String namespace); @@ -63,4 +63,5 @@ public interface DatabaseRegistryDbaasRepository { List findAllTransactionalDatabaseRegistries(String namespace); + List findAllDatabasesByFilter(List filters); } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/DatabaseRegistryRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/DatabaseRegistryRepository.java index e9465383..96f9a504 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/DatabaseRegistryRepository.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/DatabaseRegistryRepository.java @@ -1,15 +1,20 @@ package com.netcracker.cloud.dbaas.repositories.pg.jpa; +import com.netcracker.cloud.dbaas.dto.backupV2.DatabaseKind; +import com.netcracker.cloud.dbaas.dto.backupV2.DatabaseType; +import com.netcracker.cloud.dbaas.dto.backupV2.Filter; import com.netcracker.cloud.dbaas.entity.pg.DatabaseRegistry; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; -import java.util.List; -import java.util.Optional; -import java.util.SortedMap; -import java.util.UUID; +import java.util.*; +import static com.netcracker.cloud.dbaas.Constants.MICROSERVICE_NAME; +import static com.netcracker.cloud.dbaas.Constants.NAMESPACE; + +@Slf4j @ApplicationScoped @Transactional public class DatabaseRegistryRepository implements PanacheRepositoryBase { @@ -33,4 +38,53 @@ public List findAllByNamespaceAndDatabase_BgVersionNull(String public List findAllByNamespaceAndDatabase_BgVersionNotNull(String namespace) { return list("namespace = ?1 and database.bgVersion is not null", namespace); } + + public List findAllDatabasesByFilter(List filters) { + StringBuilder q = new StringBuilder( + "SELECT cl.* " + + "FROM classifier cl " + + "LEFT JOIN database d ON cl.database_id = d.id " + + "WHERE " + ); + + int index = 0; + List orBlock = new ArrayList<>(); + Map params = new HashMap<>(); + + for (Filter filter : filters) { + List query = new ArrayList<>(); + if (filter.getNamespace() != null && !filter.getNamespace().isEmpty()) { + String nsValues = "nsValues" + index; + query.add("cl.classifier #>> '{" + NAMESPACE + "}' = ANY(:" + nsValues + ")"); + params.put(nsValues, filter.getNamespace().toArray(new String[0])); + } + if (filter.getMicroserviceName() != null && !filter.getMicroserviceName().isEmpty()) { + String msValues = "msValues" + index; + query.add("cl.classifier #>> '{" + MICROSERVICE_NAME + "}' = ANY(:" + msValues + ")"); + params.put(msValues, filter.getMicroserviceName().toArray(new String[0])); + } + if (filter.getDatabaseType() != null && !filter.getDatabaseType().isEmpty()) { + String typeValues = "typeValues" + index; + query.add("cl.type = ANY(:" + typeValues + ")"); + params.put(typeValues, filter.getDatabaseType().stream().map(DatabaseType::getType).toList().toArray(new String[0])); + } + if (filter.getDatabaseKind() != null && filter.getDatabaseKind().size() == 1) { + DatabaseKind kind = filter.getDatabaseKind().getFirst(); + if (kind == DatabaseKind.CONFIGURATION) { + query.add("d.bgversion IS NOT NULL AND d.bgversion <> '' "); + } else if (kind == DatabaseKind.TRANSACTIONAL) { + query.add("(d.bgversion IS NULL OR d.bgversion = '') "); + } + } + String block = "(" + String.join(" AND ", query) + ")"; + orBlock.add(block); + index++; + } + q.append(String.join(" OR ", orBlock)); + + var query = getEntityManager() + .createNativeQuery(q.toString(), DatabaseRegistry.class); + params.forEach(query::setParameter); + return query.getResultList(); + } } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbBackupV2Service.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbBackupV2Service.java index da065cb5..494dc401 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbBackupV2Service.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/DbBackupV2Service.java @@ -38,6 +38,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import static com.netcracker.cloud.dbaas.Constants.*; @@ -427,38 +428,37 @@ public BackupStatusResponse getCurrentStatus(String backupName) { } protected Map> getAllDbByFilter(FilterCriteria filterCriteria) { - Filter filter = filterCriteria.getFilter().getFirst(); - - if (filter.getNamespace().isEmpty()) { - if (!filter.getMicroserviceName().isEmpty()) { - throw new FunctionalityNotImplemented("backup by microservice"); - } - if (!filter.getDatabaseKind().isEmpty()) { - throw new FunctionalityNotImplemented("backup by databaseKind"); - } - if (!filter.getDatabaseType().isEmpty()) { - throw new FunctionalityNotImplemented("backup by databaseType"); - } - throw new RequestValidationException(ErrorCodes.CORE_DBAAS_4043, "namespace", Source.builder().build()); - } - if (filter.getNamespace().size() > 1) { - throw new FunctionalityNotImplemented("backup by several namespace"); + int uniqKinds = (int) filterCriteria.getExclude().stream() + .flatMap(exclude -> exclude.getDatabaseKind().stream()) + .distinct() + .count(); + if (uniqKinds == 2) { + log.warn("No databases matching the filtering criteria were found during the backup"); + throw new DbNotFoundException("No databases matching the filtering criteria were found during the backup", Source.builder().build()); } - - String namespace = filter.getNamespace().getFirst(); - - List databasesRegistriesForBackup = databaseRegistryDbaasRepository - .findAnyLogDbRegistryTypeByNamespace(namespace) + List filteredDatabases = databaseRegistryDbaasRepository + .findAllDatabasesByFilter(filterCriteria.getFilter()) .stream() - .filter(this::isValidRegistry) + .filter(registry -> { + if (!isValidRegistry(registry)) + return false; + return filterCriteria.getExclude().stream().noneMatch(exclude -> { + boolean configurational = registry.getBgVersion() != null && !registry.getBgVersion().isBlank(); + return isMatches(exclude, + (String) registry.getClassifier().get(NAMESPACE), + (String) registry.getClassifier().get(MICROSERVICE_NAME), + registry.getType(), + configurational); + }); + }) .toList(); - if (databasesRegistriesForBackup.isEmpty()) { - log.warn("During backup databases that match filterCriteria not found"); - throw new DbNotFoundException("Databases that match filterCriteria not found", Source.builder().build()); + if (filteredDatabases.isEmpty()) { + log.warn("No databases matching the filtering criteria were found during the backup"); + throw new DbNotFoundException("No databases matching the filtering criteria were found during the backup", Source.builder().build()); } - return databasesRegistriesForBackup.stream() + return filteredDatabases.stream() .collect(Collectors.groupingBy(DatabaseRegistry::getDatabase)); } @@ -468,6 +468,36 @@ private boolean isValidRegistry(DatabaseRegistry registry) { && CREATED.equals(registry.getDbState().getDatabaseState()); } + private boolean isMatches(Filter filter, String namespace, String microserviceName, String type, boolean configurational) { + if (!filter.getNamespace().isEmpty() && + !filter.getNamespace().contains(namespace)) { + return false; + } + + if (!filter.getMicroserviceName().isEmpty() && + !filter.getMicroserviceName().contains(microserviceName)) { + return false; + } + + if (!filter.getDatabaseType().isEmpty() && + filter.getDatabaseType().stream().noneMatch(dt -> dt.getType().equals(type))) { + return false; + } + + if (!filter.getDatabaseKind().isEmpty()) { + return isKindMatched(configurational, filter.getDatabaseKind().getFirst()); + } + return true; + } + + private boolean isKindMatched(boolean configurational, DatabaseKind kind) { + if (kind == DatabaseKind.CONFIGURATION) + return configurational; + if (kind == DatabaseKind.TRANSACTIONAL) + return !configurational; + return true; + } + public BackupResponse getBackup(String backupName) { return mapper.toBackupResponse(getBackupOrThrowException(backupName)); } @@ -615,9 +645,6 @@ private void finalizeBackupDeletion(Backup backup, Map failedAda } public RestoreResponse restore(String backupName, RestoreRequest restoreRequest, boolean dryRun) { - if (dryRun) - throw new FunctionalityNotImplemented("dryRun"); - String restoreName = restoreRequest.getRestoreName(); if (restoreRepository.findByIdOptional(restoreName).isPresent()) { log.error("Restore with name {} already exists", restoreName); @@ -626,101 +653,83 @@ public RestoreResponse restore(String backupName, RestoreRequest restoreRequest, log.info("Start restore for backup {}", backupName); Backup backup = getBackupOrThrowException(backupName); + assertBackupStatusCompleted(backupName, backup.getStatus()); - BackupStatus backupStatus = backup.getStatus(); - if (BackupStatus.COMPLETED != backupStatus) { - log.error("Restore can`t process due to backup status {}", backupStatus); - throw new UnprocessableEntityException( - backupName, String.format("restore can`t process due to backup status %s", backupStatus), - Source.builder().build()); - } - - LockConfiguration config = new LockConfiguration( - Instant.now(), - RESTORE, - Duration.ofMinutes(2), - Duration.ofMinutes(0) - ); - - Optional optLock = lockProvider.lock(config); - - if (optLock.isEmpty()) - throw new IllegalResourceStateException("restore already running", Source.builder().build()); - - SimpleLock lock = optLock.get(); - boolean unlocked = false; - - try { - if (restoreRepository.countNotCompletedRestores() > 0) - throw new IllegalResourceStateException("another restore is being processed", Source.builder().build()); - - Restore restore = initializeFullRestoreStructure(backup, restoreRequest); - restoreRepository.save(restore); - // unlock method after save restore - lock.unlock(); - unlocked = true; + Restore restore = restoreLockWrapper(() -> { + Restore currRestore = initializeFullRestoreStructure(backup, restoreRequest); + if (!dryRun) + restoreRepository.save(currRestore); + return currRestore; + }); - // DryRun on adapters - startRestore(restore, true); + // DryRun on adapters + startRestore(restore, true); + aggregateRestoreStatus(restore); + if (!dryRun && RestoreStatus.FAILED != restore.getStatus()) { + // Real run on adapters + restore = getRestoreOrThrowException(restoreName); + startRestore(restore, false); aggregateRestoreStatus(restore); - if (RestoreStatus.FAILED != restore.getStatus()) { - // Real run on adapters - restore = getRestoreOrThrowException(restoreName); - startRestore(restore, false); - aggregateRestoreStatus(restore); - } + } + if (!dryRun) restoreRepository.save(restore); - return mapper.toRestoreResponse(restore); - } finally { - if (!unlocked) { - lock.unlock(); - } + else { + updateRestoreClassifiersOnDryRun(restore.getLogicalRestores(), restore.getExternalDatabases()); } + return mapper.toRestoreResponse(restore); + } + + private void updateRestoreClassifiersOnDryRun(List logicalRestore, List externalDatabases) { + logicalRestore.forEach(lr -> { + for (RestoreDatabase restoreDatabase : lr.getRestoreDatabases()) { + restoreDatabase.setClassifiers( + findSimilarDbByClassifier(restoreDatabase.getClassifiers(), lr.getType(), true).stream() + .toList() + ); + } + }); + externalDatabases.forEach(externalDb -> + externalDb.setClassifiers(findSimilarDbByClassifier(externalDb.getClassifiers(), externalDb.getType(), true).stream().toList()) + ); } protected List getAllDbByFilter(List backupDatabasesToFilter, FilterCriteria filterCriteria) { if (isFilterEmpty(filterCriteria)) - return backupDatabasesToFilter.stream().map(db -> new BackupDatabaseDelegate(db, db.getClassifiers())) + return backupDatabasesToFilter.stream() + .map(db -> + new BackupDatabaseDelegate( + db, + db.getClassifiers().stream() + .map(c -> new Classifier(ClassifierType.NEW, null, c)) + .toList() + ) + ) .toList(); - Filter filter = filterCriteria.getFilter().getFirst(); - - if (filter.getNamespace().isEmpty()) { - if (!filter.getMicroserviceName().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by microservice"); - } - if (!filter.getDatabaseKind().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by databaseKind"); - } - if (!filter.getDatabaseType().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by databaseType"); - } - throw new RequestValidationException(ErrorCodes.CORE_DBAAS_4043, "namespace", Source.builder().build()); - } - if (filter.getNamespace().size() > 1) { - throw new FunctionalityNotImplemented("restoration by several namespace"); - } - String namespace = filter.getNamespace().getFirst(); - // Filter BackupDatabase by namespace List databaseDelegateList = backupDatabasesToFilter.stream() - .map(backupDatabase -> { - List> filteredClassifiers = backupDatabase.getClassifiers().stream() - .filter(classifier -> namespace.equals(classifier.get(NAMESPACE))) - .map(classifier -> (SortedMap) new TreeMap<>(classifier)) - .toList(); + .map(db -> { + List filteredClassifiers = db.getClassifiers().stream() + .filter(classifier -> { + String namespace = (String) classifier.get(NAMESPACE); + String microserviceName = (String) classifier.get(MICROSERVICE_NAME); + String type = db.getLogicalBackup().getType(); + boolean configurational = db.isConfigurational(); + return filterCriteria.getFilter().stream().anyMatch(filter -> isMatches(filter, namespace, microserviceName, type, configurational)) + && filterCriteria.getExclude().stream().noneMatch(ex -> isMatches(ex, namespace, microserviceName, type, configurational)); + }) + .map(c -> new Classifier(ClassifierType.NEW, null, c)) + .toList(); - if (filteredClassifiers.isEmpty()) - return null; + if (filteredClassifiers.isEmpty()) { + return null; + } - return new BackupDatabaseDelegate( - backupDatabase, - filteredClassifiers - ); - } - ) + return new BackupDatabaseDelegate(db, filteredClassifiers); + }) .filter(Objects::nonNull) .toList(); + if (databaseDelegateList.isEmpty()) { log.warn("During restore databases that match filterCriteria not found"); throw new DbNotFoundException("Databases that match filterCriteria not found", Source.builder().build()); @@ -793,15 +802,15 @@ protected Restore initializeFullRestoreStructure( .mapToInt(lr -> lr.getRestoreDatabases().size()) .sum(); - log.info("Restore structure initialized: restoreName={}, logicalRestores={}, restoreDatabases={}", - restore.getName(), logicalRestores.size(), totalDatabases); + log.info("Restore structure initialized: restoreName={}, logicalRestores={}, restoreDatabases={}, externalDatabases={}", + restore.getName(), logicalRestores.size(), totalDatabases, externalDatabases.size()); return restore; } - private List validateAndFilterExternalDb(List externalDatabases, - ExternalDatabaseStrategy strategy, - FilterCriteria filterCriteria) { + protected List validateAndFilterExternalDb(List externalDatabases, + ExternalDatabaseStrategy strategy, + FilterCriteria filterCriteria) { if (externalDatabases == null || externalDatabases.isEmpty()) return List.of(); @@ -825,41 +834,50 @@ private List validateAndFilterExternalDb(List { log.info("Including external databases to restore by strategy: {}", ExternalDatabaseStrategy.INCLUDE); if (isFilterEmpty(filterCriteria)) - yield mapper.toRestoreExternalDatabases(externalDatabases); + yield mapper.toRestoreExternalDatabases( + externalDatabases.stream() + .map(db -> + new BackupExternalDelegate(db, db.getClassifiers().stream() + .map(c -> + new Classifier(ClassifierType.NEW, null, c) + ) + .toList() + ) + ) + .toList() + ); - Filter filter = filterCriteria.getFilter().getFirst(); + yield externalDatabases.stream() + .map(db -> { + List filteredClassifiers = db.getClassifiers().stream() + .filter(classifier -> { + String namespace = (String) classifier.get(NAMESPACE); + String microserviceName = (String) classifier.get(MICROSERVICE_NAME); + String type = db.getType(); + return filterCriteria.getFilter().stream().anyMatch(filter -> isMatches(filter, namespace, microserviceName, type, false)) + && filterCriteria.getExclude().stream().noneMatch(ex -> isMatches(ex, namespace, microserviceName, type, false)); + }) + .map(c -> new Classifier(ClassifierType.NEW, null, c)) + .toList(); - if (filter.getNamespace().isEmpty()) { - if (!filter.getMicroserviceName().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by microservice"); - } - if (!filter.getDatabaseKind().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by databaseKind"); - } - if (!filter.getDatabaseType().isEmpty()) { - throw new FunctionalityNotImplemented("restoration by databaseType"); - } - throw new RequestValidationException(ErrorCodes.CORE_DBAAS_4043, "namespace", Source.builder().build()); - } - if (filter.getNamespace().size() > 1) { - throw new FunctionalityNotImplemented("restoration by several namespace"); - } - String namespace = filter.getNamespace().getFirst(); - yield mapper.toRestoreExternalDatabases(externalDatabases).stream() - .filter(db -> db.getClassifiers().stream() - .anyMatch(classifier -> - namespace.equals(classifier.get(NAMESPACE))) - ).toList(); + if (filteredClassifiers.isEmpty()) { + return null; + } + + return mapper.toRestoreExternalDatabase(new BackupExternalDelegate(db, filteredClassifiers)); + }) + .filter(Objects::nonNull) + .toList(); } }; } private List executeMappingForExternalDb(List externalDatabases, Mapping mapping) { + Set> uniqueClassifiers = new HashSet<>(); return externalDatabases.stream() .peek(db -> { - Set> uniqueClassifiers = new HashSet<>(); - List> updatedClassifiers = db.getClassifiers().stream() + List updatedClassifiers = db.getClassifiers().stream() .map(classifier -> updateAndValidateClassifier(classifier, mapping, uniqueClassifiers)) .toList(); db.setClassifiers(updatedClassifiers); @@ -873,8 +891,8 @@ private List createRestoreDatabases( return backupDatabases.stream() .map(delegatedBackupDatabase -> { BackupDatabase backupDatabase = delegatedBackupDatabase.backupDatabase(); - List> classifiers = delegatedBackupDatabase.classifiers(); - String namespace = (String) classifiers.getFirst().get(NAMESPACE); + List classifiers = delegatedBackupDatabase.classifiers(); + String namespace = classifiers.getFirst().getClassifier() != null ? (String) classifiers.getFirst().getClassifier().get(NAMESPACE) : (String) classifiers.getFirst().getClassifierBeforeMapper().get(NAMESPACE); String bgVersion = null; if (backupDatabase.isConfigurational()) { Optional bgNamespace = bgNamespaceRepository.findBgNamespaceByNamespace(namespace); @@ -898,15 +916,17 @@ private List createRestoreDatabases( .toList(); } - private SortedMap updateClassifier(SortedMap classifier, Mapping mapping) { - String targetNamespace = getValue(mapping.getNamespaces(), (String) classifier.get(NAMESPACE)); - String targetTenant = getValue(mapping.getTenants(), (String) classifier.get(TENANT_ID)); + private Classifier updateClassifier(Classifier classifier, Mapping mapping) { + String targetNamespace = getValue(mapping.getNamespaces(), (String) classifier.getClassifierBeforeMapper().get(NAMESPACE)); + String targetTenant = getValue(mapping.getTenants(), (String) classifier.getClassifierBeforeMapper().get(TENANT_ID)); - SortedMap updatedClassifier = new TreeMap<>(classifier); + SortedMap updatedClassifier = new TreeMap<>(classifier.getClassifierBeforeMapper()); updatedClassifier.put(NAMESPACE, targetNamespace); if (targetTenant != null) updatedClassifier.put(TENANT_ID, targetTenant); - return updatedClassifier; + if (!targetNamespace.equals(classifier.getClassifierBeforeMapper().get(NAMESPACE))) + classifier.setClassifier(updatedClassifier); + return classifier; } private String getValue(Map map, String oldValue) { @@ -919,39 +939,35 @@ private String getValue(Map map, String oldValue) { private Map> groupBackupDatabasesByTypeAndAdapter( List backupDatabases, Mapping mapping) { - + Set> uniqueClassifiers = new HashSet<>(); return backupDatabases.stream() - .map(db -> mapToPhysicalDatabaseEntry(db, mapping)) - .filter(Objects::nonNull) + .map(db -> mapToPhysicalDatabaseEntry(db, mapping, uniqueClassifiers)) .collect(Collectors.groupingBy( Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList()) )); } - private Map.Entry mapToPhysicalDatabaseEntry( - BackupDatabaseDelegate db, Mapping mapping) { - List> classifiers = db.classifiers(); - if (classifiers.isEmpty()) { - return null; - } - - SortedMap firstClassifier = classifiers.getFirst(); + private Map.Entry mapToPhysicalDatabaseEntry(BackupDatabaseDelegate db, + Mapping mapping, + Set> uniqueClassifiers) { + List classifiers = db.classifiers(); + SortedMap firstClassifier = classifiers.getFirst().getClassifierBeforeMapper(); String targetNamespace = (String) firstClassifier.get(NAMESPACE); String microserviceName = (String) firstClassifier.get(MICROSERVICE_NAME); // Mapping classifiers if (mapping != null && mapping.getNamespaces() != null) { - Set> uniqueClassifiers = new HashSet<>(); classifiers = db.classifiers().stream() .map(classifier -> updateAndValidateClassifier(classifier, mapping, uniqueClassifiers)) .toList(); // Find the first classifier whose namespace exists in the mapping SortedMap matchedClassifier = classifiers.stream() + .map(Classifier::getClassifierBeforeMapper) .filter(c -> mapping.getNamespaces().containsKey((String) c.get(NAMESPACE))) .findFirst() - .orElse(classifiers.getFirst()); + .orElse(classifiers.getFirst().getClassifierBeforeMapper()); String oldNamespace = (String) matchedClassifier.get(NAMESPACE); targetNamespace = mapping.getNamespaces().getOrDefault(oldNamespace, oldNamespace); @@ -974,17 +990,17 @@ private Map.Entry mapToPhysicalDatabas return Map.entry(physicalDatabase, new BackupDatabaseDelegate(db.backupDatabase(), classifiers)); } - private SortedMap updateAndValidateClassifier( - SortedMap classifier, + private Classifier updateAndValidateClassifier( + Classifier classifier, Mapping mapping, Set> uniqueClassifiers) { - SortedMap updatedClassifier = updateClassifier(classifier, mapping); + Classifier updatedClassifier = updateClassifier(classifier, mapping); // To prevent collision during mapping - if (!uniqueClassifiers.add(updatedClassifier)) { + if (!uniqueClassifiers.add(updatedClassifier.getClassifier())) { String msg = String.format( - "Duplicate classifier detected after mapping: classifier='%s', mapping='%s'. " + + "Duplicate classifier detected after mapping: classifier='%s', classifierBeforeMapping='%s'. " + "Ensure all classifiers remain unique after mapping.", - classifier, mapping); + classifier.getClassifier(), classifier.getClassifierBeforeMapper()); log.error(msg); throw new IllegalResourceStateException(msg, Source.builder().build()); } @@ -994,26 +1010,33 @@ private SortedMap updateAndValidateClassifier( protected void startRestore(Restore restore, boolean dryRun) { List logicalRestores = restore.getLogicalRestores(); log.info("Starting requesting adapters to restore startup process: restore={}, dryRun={} logicalRestoreCount={}", - restore.getName(), dryRun, restore.getLogicalRestores().size()); + restore.getName(), dryRun, logicalRestores.size()); + String storageName = restore.getStorageName(); + String blobPath = restore.getBlobPath(); + List> futures = logicalRestores.stream() .map(logicalRestore -> - CompletableFuture.supplyAsync(asyncOperations.wrapWithContext(() -> - logicalRestore(logicalRestore, dryRun))) - .thenAccept(response -> - refreshLogicalRestoreState(logicalRestore, response)) - .exceptionally(throwable -> { - logicalRestore.setStatus(RestoreTaskStatus.FAILED); - logicalRestore.setErrorMessage(extractErrorMessage(throwable)); - log.error("Logical restore failed: adapterId={}, error={}", - logicalRestore.getAdapterId(), logicalRestore.getErrorMessage()); - return null; - }) + runLogicalRestoreAsync(logicalRestore, logicalRestore.getRestoreDatabases(), storageName, blobPath, dryRun) ) .toList(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + private CompletableFuture runLogicalRestoreAsync(LogicalRestore logicalRestore, List restoreDatabases, String storageName, String blobPath, boolean dryRun) { + return CompletableFuture.supplyAsync(asyncOperations.wrapWithContext(() -> + logicalRestore(logicalRestore.getLogicalRestoreName(), logicalRestore, restoreDatabases, storageName, blobPath, dryRun))) + .thenAccept(response -> + refreshLogicalRestoreState(logicalRestore, response)) + .exceptionally(throwable -> { + logicalRestore.setStatus(RestoreTaskStatus.FAILED); + logicalRestore.setErrorMessage(extractErrorMessage(throwable)); + log.error("Logical restore failed: adapterId={}, error={}", + logicalRestore.getAdapterId(), logicalRestore.getErrorMessage()); + return null; + }); + } + private void refreshLogicalRestoreState(LogicalRestore logicalRestore, LogicalRestoreAdapterResponse response) { log.info("Starting LogicalRestore state update [restoreName={}, logicalRestoreName={}]", logicalRestore.getRestore().getName(), @@ -1058,43 +1081,35 @@ private void refreshLogicalRestoreState(LogicalRestore logicalRestore, LogicalRe logicalRestore.getLogicalRestoreName(), logicalRestore.getStatus(), logicalRestore.getErrorMessage()); } - private LogicalRestoreAdapterResponse logicalRestore(LogicalRestore logicalRestore, boolean dryRun) { - String logicalBackupName = logicalRestore.getRestoreDatabases().getFirst() + private LogicalRestoreAdapterResponse logicalRestore(String logicalRestoreName, LogicalRestore logicalRestore, List restoreDatabases, String storageName, String blobPath, boolean dryRun) { + String logicalBackupName = restoreDatabases.getFirst() .getBackupDatabase() .getLogicalBackup() .getLogicalBackupName(); - List> databases = buildRestoreDatabases(logicalRestore); - Restore restore = logicalRestore.getRestore(); - RetryPolicy retryPolicy = buildRetryPolicy(logicalRestore.getLogicalRestoreName(), RESTORE_OPERATION); + List> databases = buildRestoreDatabases(restoreDatabases); + RetryPolicy retryPolicy = buildRetryPolicy(logicalRestoreName, RESTORE_OPERATION); - try { - return Failsafe.with(retryPolicy) - .get(() -> executeRestore(logicalRestore, logicalBackupName, restore, databases, dryRun)); - } catch (Exception e) { - log.error("Logical restore startup for adapterId={} failed, restore={}", logicalRestore.getAdapterId(), restore.getName()); - throw new BackupExecutionException( - String.format("Logical restore startup for adapterId=%s failed, restore=%s", - logicalRestore.getAdapterId(), restore.getName()), e); - } + return Failsafe.with(retryPolicy) + .get(() -> executeRestore(logicalRestore, logicalBackupName, storageName, blobPath, databases, dryRun)); } - private List> buildRestoreDatabases(LogicalRestore logicalRestore) { - return logicalRestore.getRestoreDatabases().stream() + private List> buildRestoreDatabases(List restoreDatabases) { + return restoreDatabases.stream() .map(restoreDatabase -> { String namespace = restoreDatabase.getClassifiers().stream() - .map(i -> (String) i.get(NAMESPACE)) + .map(c -> c.getClassifier() != null ? (String) c.getClassifier().get(NAMESPACE) : (String) c.getClassifierBeforeMapper().get(NAMESPACE)) .findFirst() .orElse(""); String microserviceName = restoreDatabase.getClassifiers().stream() - .map(i -> (String) i.get(MICROSERVICE_NAME)) + .map(c -> c.getClassifier() != null ? (String) c.getClassifier().get(MICROSERVICE_NAME) : (String) c.getClassifierBeforeMapper().get(MICROSERVICE_NAME)) .findFirst() .orElse(""); return Map.of( MICROSERVICE_NAME, microserviceName, - DATABASE_NAME, restoreDatabase.getName(), + DATABASE_NAME, restoreDatabase.getBackupDatabase().getName(), NAMESPACE, namespace ); }) @@ -1104,7 +1119,8 @@ private List> buildRestoreDatabases(LogicalRestore logicalRe private LogicalRestoreAdapterResponse executeRestore( LogicalRestore logicalRestore, String logicalBackupName, - Restore restore, + String storageName, + String blobPath, List> databases, boolean dryRun ) { @@ -1113,7 +1129,7 @@ private LogicalRestoreAdapterResponse executeRestore( LogicalRestoreAdapterResponse result = adapter.restoreV2( logicalBackupName, dryRun, - new RestoreAdapterRequest(restore.getStorageName(), restore.getBlobPath(), databases) + new RestoreAdapterRequest(storageName, blobPath, databases) ); if (result == null) { @@ -1164,14 +1180,19 @@ protected List ensureUsers(String adapterId, DbaasAdapter adapter = physicalDatabasesService.getAdapterById(adapterId); RetryPolicy retryPolicy = buildRetryPolicy(dbName, ENSURE_USER_OPERATION); - log.info("Ensuring {} users for databaseName=[{}] via adapter [{}]", + log.info("Start ensure {} users for database=[{}] via adapter [{}]", users.size(), dbName, adapterId); return users.stream() .map(user -> { try { return Failsafe.with(retryPolicy) - .get(() -> adapter.ensureUser(user.getName(), null, dbName, user.getRole())); + .get(() -> { + EnsuredUser ensuredUser = adapter.ensureUser(user.getName(), null, dbName, user.getRole()); + log.info("User ensured for database=[{}], user information=[name={}, connectionProperties={}]", + dbName, ensuredUser.getName(), ensuredUser.getConnectionProperties()); + return ensuredUser; + }); } catch (Exception e) { log.error("Failed to ensure user {} in database {}", user.getName(), dbName, e); throw new BackupExecutionException( @@ -1275,17 +1296,15 @@ protected void initializeLogicalDatabasesFromRestore(Restore restore, Map { String type = logicalRestore.getType(); - Set> classifiers = new HashSet<>(); - log.info("Processing restoreDatabase={}", restoreDatabase.getName()); - findSimilarDbByClassifier(classifiers, restoreDatabase.getClassifiers(), type); + Set classifiers = findSimilarDbByClassifier(restoreDatabase.getClassifiers(), type, false); String adapterId = logicalRestore.getAdapterId(); String physicalDatabaseId = physicalDatabasesService.getByAdapterId(adapterId).getPhysicalDatabaseIdentifier(); List ensuredUsers = dbNameToEnsuredUsers.get(restoreDatabase.getName()); Database newDatabase = createLogicalDatabase( restoreDatabase.getName(), restoreDatabase.getSettings(), - classifiers, + classifiers.stream().map(c -> c.getClassifier() != null ? c.getClassifier() : c.getClassifierBeforeMapper()).collect(Collectors.toSet()), type, false, false, @@ -1298,20 +1317,19 @@ protected void initializeLogicalDatabasesFromRestore(Restore restore, Map { log.info("Processing externalDatabase={}, type={}", externalDatabase.getName(), externalDatabase.getType()); String type = externalDatabase.getType(); - Set> classifiers = new HashSet<>(); - - findSimilarDbByClassifier(classifiers, externalDatabase.getClassifiers(), type); + Set classifiers = findSimilarDbByClassifier(externalDatabase.getClassifiers(), type, false); Database newDatabase = createLogicalDatabase( externalDatabase.getName(), null, - classifiers, + classifiers.stream().map(c -> c.getClassifier() != null ? c.getClassifier() : c.getClassifierBeforeMapper()).collect(Collectors.toSet()), type, true, true, @@ -1319,7 +1337,8 @@ protected void initializeLogicalDatabasesFromRestore(Restore restore, Map> uniqueClassifiers, - List> classifiers, - String type) { + private Set findSimilarDbByClassifier(List classifiers, + String type, boolean dryRun) { + Set uniqueClassifiers = new HashSet<>(); classifiers.forEach(classifier -> { - uniqueClassifiers.add(new TreeMap<>(classifier)); - log.debug("Classifier candidate: {}", classifier); - databaseRegistryDbaasRepository - .getDatabaseByClassifierAndType(classifier, type) - .ifPresent(dbRegistry -> { - Database db = dbRegistry.getDatabase(); - log.info("Found existing database {} for classifier {}", db.getId(), classifier); - List> existClassifiers = db.getDatabaseRegistry().stream() - .map(AbstractDatabaseRegistry::getClassifier) - .map(TreeMap::new) - .toList(); - - uniqueClassifiers.addAll(existClassifiers); - dBaaService.markDatabasesAsOrphan(dbRegistry); - log.info("Database {} marked as orphan", db.getId()); - databaseRegistryDbaasRepository.saveAnyTypeLogDb(dbRegistry); - }); + SortedMap currClassifier = classifier.getClassifier() != null ? classifier.getClassifier() + : classifier.getClassifierBeforeMapper(); + log.debug("Classifier candidate: {}", currClassifier); + + Optional optionalDatabaseRegistry = databaseRegistryDbaasRepository + .getDatabaseByClassifierAndType(currClassifier, type); + + if (optionalDatabaseRegistry.isPresent()) { + DatabaseRegistry dbRegistry = optionalDatabaseRegistry.get(); + classifier.setType(ClassifierType.REPLACED); + Database db = dbRegistry.getDatabase(); + log.info("Found existing database {} for classifier {}", db.getId(), currClassifier); + Set existClassifiers = db.getDatabaseRegistry().stream() + .map(AbstractDatabaseRegistry::getClassifier) + .map(TreeMap::new) + .map(c -> currClassifier.equals(c) + ? classifier + : new Classifier(ClassifierType.TRANSIENT_REPLACED, c, null) + ) + .collect(Collectors.toSet()); + + uniqueClassifiers.addAll(existClassifiers); + if (!dryRun) { + dBaaService.markDatabasesAsOrphan(dbRegistry); + log.info("Database {} marked as orphan", db.getId()); + databaseRegistryDbaasRepository.saveAnyTypeLogDb(dbRegistry); + } + } else + uniqueClassifiers.add(classifier); }); + return uniqueClassifiers; } private Database createLogicalDatabase(String dbName, @@ -1431,7 +1463,61 @@ public void deleteRestore(String restoreName) { } public RestoreResponse retryRestore(String restoreName) { - throw new FunctionalityNotImplemented("retry restore functionality not implemented yet"); + // Check existence of restore by restoreName + Restore restore = getRestoreOrThrowException(restoreName); + // Check if the status of the restor has FAILED + if (RestoreStatus.FAILED != restore.getStatus()) { + throw new UnprocessableEntityException( + restoreName, + String.format( + "has invalid status '%s'. Only %s restores can be processed.", + restore.getStatus(), RestoreStatus.FAILED + ), + Source.builder().build()); + } + + assertBackupStatusCompleted(restore.getBackup().getName(), restore.getBackup().getStatus()); + + return restoreLockWrapper(() -> { + retryRestore(restore); + aggregateRestoreStatus(restore); + restoreRepository.save(restore); + return mapper.toRestoreResponse(restore); + }); + } + + private void assertBackupStatusCompleted(String backupName, BackupStatus status) { + // Check if the status of backup has COMPLETED + if (BackupStatus.COMPLETED != status) { + log.error("Restore can`t process due to backup status {}", status); + throw new UnprocessableEntityException( + backupName, String.format("restore can`t process due to backup status %s", status), + Source.builder().build()); + } + } + + private void retryRestore(Restore restore) { + List failedLogicalRestores = restore.getLogicalRestores() + .stream() + .filter(logicalRestore -> RestoreTaskStatus.FAILED == logicalRestore.getStatus()) + .toList(); + + log.info("Starting retry restore process: restore={}, failedLogicalRestoreCount={}", + restore.getName(), failedLogicalRestores.size()); + String storageName = restore.getStorageName(); + String blobPath = restore.getBlobPath(); + + List> futures = failedLogicalRestores.stream() + .map(logicalRestore -> { + List failedRestoreDatabase = logicalRestore.getRestoreDatabases().stream() + .filter(db -> RestoreTaskStatus.FAILED == db.getStatus()) + .toList(); + return runLogicalRestoreAsync(logicalRestore, failedRestoreDatabase, storageName, blobPath, false); + } + ) + .toList(); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } protected Map> validateAndFilterDatabasesForBackup(Map> databasesForBackup, @@ -1529,6 +1615,31 @@ private Restore getRestoreOrThrowException(String restoreName) { .orElseThrow(() -> new BackupRestorationNotFoundException(restoreName, Source.builder().build())); } + private T restoreLockWrapper(Supplier action) { + // Only one retry restore operation able to process + LockConfiguration config = new LockConfiguration( + Instant.now(), + RESTORE, + Duration.ofMinutes(2), + Duration.ofMinutes(0)); + + Optional optLock = lockProvider.lock(config); + + if (optLock.isEmpty()) + throw new IllegalResourceStateException("restore already running", Source.builder().build()); + // Start locking action + SimpleLock lock = optLock.get(); + boolean unlocked = false; + try { + if (restoreRepository.countNotCompletedRestores() > 0) + throw new IllegalResourceStateException("another restore is being processed", Source.builder().build()); + return action.get(); + } finally { + if (!unlocked) + lock.unlock(); + } + } + private RetryPolicy buildRetryPolicy(String name, String operation) { return new RetryPolicy<>() .handle(WebApplicationException.class) @@ -1557,11 +1668,7 @@ private String extractErrorMessage(Throwable throwable) { } private boolean isFilterEmpty(FilterCriteria filterCriteria) { - if (filterCriteria == null || filterCriteria.getFilter() == null) - return true; - - return filterCriteria.getFilter().isEmpty() - || filterCriteria.getFilter().stream().allMatch(this::isSingleFilterEmpty); + return filterCriteria == null || filterCriteria.getFilter() == null || filterCriteria.getFilter().isEmpty(); } private boolean isSingleFilterEmpty(Filter f) { diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/BackupGroup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/BackupGroup.java deleted file mode 100644 index 82a744a8..00000000 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/BackupGroup.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.netcracker.cloud.dbaas.utils.validation; - -public interface BackupGroup { -} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilter.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilter.java new file mode 100644 index 00000000..8cf82b91 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilter.java @@ -0,0 +1,20 @@ +package com.netcracker.cloud.dbaas.utils.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = NotEmptyFilterValidation.class) +public @interface NotEmptyFilter { + String message() default "Filter must have at least one non-null field"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilterValidation.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilterValidation.java new file mode 100644 index 00000000..d41c3a34 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/NotEmptyFilterValidation.java @@ -0,0 +1,21 @@ +package com.netcracker.cloud.dbaas.utils.validation; + +import com.netcracker.cloud.dbaas.dto.backupV2.Filter; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.List; + +public class NotEmptyFilterValidation implements ConstraintValidator { + @Override + public boolean isValid(Filter value, ConstraintValidatorContext context) { + return isValid(value.getNamespace()) || + isValid(value.getMicroserviceName()) || + isValid(value.getDatabaseType()) || + isValid(value.getDatabaseKind()); + } + + private boolean isValid(List list) { + return list != null && !list.isEmpty(); + } +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/RestoreGroup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/RestoreGroup.java deleted file mode 100644 index 201d455e..00000000 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/RestoreGroup.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.netcracker.cloud.dbaas.utils.validation; - -public interface RestoreGroup { -} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/BackupGroup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/BackupGroup.java new file mode 100644 index 00000000..b2c92397 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/BackupGroup.java @@ -0,0 +1,4 @@ +package com.netcracker.cloud.dbaas.utils.validation.group; + +public interface BackupGroup { +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/RestoreGroup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/RestoreGroup.java new file mode 100644 index 00000000..9c507d16 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/validation/group/RestoreGroup.java @@ -0,0 +1,4 @@ +package com.netcracker.cloud.dbaas.utils.validation.group; + +public interface RestoreGroup { +} diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2ControllerTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2ControllerTest.java index 3332a210..5fb38ef9 100644 --- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2ControllerTest.java +++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/v3/DatabaseBackupV2ControllerTest.java @@ -86,7 +86,7 @@ void initiateBackup_invalidDto() { .statusCode(BAD_REQUEST.getStatusCode()) .body("message", allOf( containsString("backupName: must not be blank"), - containsString("filter: must not be null") + containsString("filter: size must be between 1 and 2147483647") )); verify(dbBackupV2Service, times(0)).backup(any(), anyBoolean()); @@ -139,6 +139,49 @@ void initiateBackup_backupAlreadyExists() { verify(dbBackupV2Service, times(1)).backup(backupRequest, false); } + @Test + void initiateBackup_emptyFilterCase(){ + String namespace = "namespace"; + String backupName = "backupName"; + + BackupRequest backupRequest = createBackupRequest(namespace, backupName); + FilterCriteria emptyFilterCriteria = backupRequest.getFilterCriteria(); + emptyFilterCriteria.setFilter(List.of(new Filter())); + emptyFilterCriteria.setExclude(List.of(new Filter())); + + given().auth().preemptive().basic("backup_manager", "backup_manager") + .contentType(ContentType.JSON) + .body(backupRequest) + .when().post("/backup") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .body("reason", equalTo("Request does not contain required fields")) + .body("message", equalTo("filter[0]: Filter must have at least one non-null field")); + verify(dbBackupV2Service, times(0)).backup(backupRequest, false); + } + + @Test + void restoreBackup_emptyFilterCase(){ + String namespace = "namespace"; + String restoreName = "restoreName"; + String backupName = "backupName"; + + RestoreRequest restoreRequest = createRestoreRequest(namespace, restoreName); + FilterCriteria emptyFilterCriteria = restoreRequest.getFilterCriteria(); + emptyFilterCriteria.setFilter(List.of(new Filter())); + emptyFilterCriteria.setExclude(List.of(new Filter())); + + given().auth().preemptive().basic("backup_manager", "backup_manager") + .contentType(ContentType.JSON) + .body(restoreRequest) + .pathParam("backupName", backupName) + .when().post("/backup/{backupName}/restore") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .body("reason", equalTo("Request does not contain required fields")) + .body("message", equalTo("filter[0]: Filter must have at least one non-null field")); + verify(dbBackupV2Service, times(0)).restore(backupName, restoreRequest, false); + } @Test void getBackupStatus_validBackupNameCase() { String backupName = "test-backup-name"; @@ -330,6 +373,22 @@ public static BackupRequest createBackupRequest(String namespace, String backupN return dto; } + public static RestoreRequest createRestoreRequest(String namespace, String restoreName) { + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + + RestoreRequest dto = new RestoreRequest(); + dto.setRestoreName(restoreName); + dto.setFilterCriteria(filterCriteria); + dto.setExternalDatabaseStrategy(ExternalDatabaseStrategy.FAIL); + dto.setBlobPath("path"); + dto.setStorageName("storageName"); + return dto; + } + private BackupResponse createBackupResponse(String backupName) { String storageName = "storageName"; SortedMap sortedMap = new TreeMap<>(); diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/DbBackupV2ServiceTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/DbBackupV2ServiceTest.java index 70a4f6a5..e4ceeb50 100644 --- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/DbBackupV2ServiceTest.java +++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/DbBackupV2ServiceTest.java @@ -2,6 +2,7 @@ import com.netcracker.cloud.dbaas.dto.EnsuredUser; import com.netcracker.cloud.dbaas.dto.backupV2.*; +import com.netcracker.cloud.dbaas.entity.dto.backupV2.BackupDatabaseDelegate; import com.netcracker.cloud.dbaas.entity.dto.backupV2.LogicalBackupAdapterResponse; import com.netcracker.cloud.dbaas.entity.dto.backupV2.LogicalRestoreAdapterResponse; import com.netcracker.cloud.dbaas.entity.pg.*; @@ -891,7 +892,6 @@ void restore_withoutMapping_finishedWithStatusCompleted() { assertNotNull(restoreResponse); assertEquals(restoreName, restoreResponse.getRestoreName()); assertEquals(RestoreStatus.IN_PROGRESS, restoreResponse.getStatus()); - assertEquals(2, restoreResponse.getDuration()); dbBackupV2Service.checkRestoresAsync(); Restore restore = restoreRepository.findById(restoreName); @@ -929,7 +929,7 @@ void restore_withoutMapping_finishedWithStatusCompleted() { assertEquals(1, restoreDatabase1.getClassifiers().size()); assertEquals(1, restoreDatabase1.getDuration()); - SortedMap classifier = restoreDatabase1.getClassifiers().getFirst(); + SortedMap classifier = restoreDatabase1.getClassifiers().getFirst().getClassifierBeforeMapper(); assertTrue( namespace.equals(classifier.get(NAMESPACE)) && microserviceName1.equals(classifier.get(MICROSERVICE_NAME)) @@ -954,7 +954,7 @@ void restore_withoutMapping_finishedWithStatusCompleted() { assertEquals(1, restoreDatabase2.getClassifiers().size()); assertEquals(1, restoreDatabase2.getDuration()); - classifier = restoreDatabase2.getClassifiers().getFirst(); + classifier = restoreDatabase2.getClassifiers().getFirst().getClassifierBeforeMapper(); assertTrue( namespace.equals(classifier.get(NAMESPACE)) && microserviceName2.equals(classifier.get(MICROSERVICE_NAME)) @@ -1172,7 +1172,6 @@ void restore_withMapping_finishedWithStatusCompleted() { assertNotNull(restoreResponse); assertEquals(restoreName, restoreResponse.getRestoreName()); assertEquals(RestoreStatus.IN_PROGRESS, restoreResponse.getStatus()); - assertEquals(2, restoreResponse.getDuration()); dbBackupV2Service.checkRestoresAsync(); Restore restore = restoreRepository.findById(restoreName); @@ -1195,7 +1194,7 @@ void restore_withMapping_finishedWithStatusCompleted() { assertEquals(externalDbName, restoreExternalDatabase.getName()); assertEquals(postgresqlType, restoreExternalDatabase.getType()); - SortedMap externalClassifier = restoreExternalDatabase.getClassifiers().getFirst(); + SortedMap externalClassifier = restoreExternalDatabase.getClassifiers().getFirst().getClassifier(); assertTrue( mappedNamespace.equals(externalClassifier.get(NAMESPACE)) && microserviceName3.equals(externalClassifier.get(MICROSERVICE_NAME)) && @@ -1221,7 +1220,7 @@ void restore_withMapping_finishedWithStatusCompleted() { assertEquals(1, restoreDatabase1.getClassifiers().size()); assertEquals(1, restoreDatabase1.getDuration()); - SortedMap classifier = restoreDatabase1.getClassifiers().getFirst(); + SortedMap classifier = restoreDatabase1.getClassifiers().getFirst().getClassifier(); assertTrue( mappedNamespace.equals(classifier.get(NAMESPACE)) && microserviceName1.equals(classifier.get(MICROSERVICE_NAME)) && @@ -1247,7 +1246,7 @@ void restore_withMapping_finishedWithStatusCompleted() { assertEquals(1, restoreDatabase2.getClassifiers().size()); assertEquals(1, restoreDatabase2.getDuration()); - classifier = restoreDatabase2.getClassifiers().getFirst(); + classifier = restoreDatabase2.getClassifiers().getFirst().getClassifier(); assertTrue( mappedNamespace.equals(classifier.get(NAMESPACE)) && microserviceName2.equals(classifier.get(MICROSERVICE_NAME)) && @@ -1344,11 +1343,11 @@ void restore_mappingInvokeCollision() { SortedMap classifier1 = getClassifier(namespace, microserviceName, tenantId); SortedMap classifier2 = getClassifier(namespace, microserviceName, anotherTenantId); - + SortedMap mappedClassifier = getClassifier(mappedNamespace, microserviceName, anotherTenantId); String msg = String.format( - "Resource has illegal state: Duplicate classifier detected after mapping: classifier='%s', mapping='%s'. " + + "Resource has illegal state: Duplicate classifier detected after mapping: classifier='%s', classifierBeforeMapping='%s'. " + "Ensure all classifiers remain unique after mapping.", - classifier2, mapping); + mappedClassifier, classifier2); BackupDatabase backupDatabase = getBackupDatabase(dbName, List.of(classifier1, classifier2), false, BackupTaskStatus.COMPLETED, ""); LogicalBackup logicalBackup = getLogicalBackup(logicalBackupName, adapterId, postgresType, List.of(backupDatabase), BackupTaskStatus.COMPLETED, ""); @@ -1580,6 +1579,667 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { } + @Test + void restore_dryRun() { + String restoreName = "restoreName"; + String backupName = "backupName"; + String logicalBackupName = "logicalBackupName"; + String logicalRestoreName = "logicalRestoreName"; + + String dbName = "dbName"; + String newName = "newName"; + String adapterId = "adapterId"; + + String externalDbName = "externalDbName"; + + String namespace = "namespace"; + String mappedNamespace = "mappedNamespace"; + String anotherNamespace = "anotherNamespace"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + + String tenantId = "tenantId"; + String anotherTenantId = "tenantId"; + String mappedTenantId = "mappedTenantId"; + String postgresType = "postgresql"; + + // Database with registries that exists in another namespace + Database database = getDatabase(adapterId, newName, false, false, null); + Database externalDb = getDatabase(null, externalDbName, true, false, null); + DatabaseRegistry registry1 = getDatabaseRegistry(database, mappedNamespace, microserviceName1, mappedTenantId, postgresType); + DatabaseRegistry registry2 = getDatabaseRegistry(database, anotherNamespace, microserviceName1, anotherTenantId, postgresType); + DatabaseRegistry externalRegistry = getDatabaseRegistry(externalDb, mappedNamespace, microserviceName2, mappedTenantId, postgresType); + + databaseRegistryDbaasRepository.saveAnyTypeLogDb(registry1); + databaseRegistryDbaasRepository.saveExternalDatabase(externalRegistry); + + BackupExternalDatabase externalDatabase = getBackupExternalDatabase(externalDbName, postgresType, List.of(getClassifier(namespace, microserviceName2, tenantId))); + BackupDatabase backupDatabase = getBackupDatabase(dbName, List.of(getClassifier(namespace, microserviceName1, tenantId)), false, BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup = getLogicalBackup(logicalBackupName, adapterId, postgresType, List.of(backupDatabase), BackupTaskStatus.COMPLETED, null); + Backup backup = getBackup(backupName, ExternalDatabaseStrategy.INCLUDE, getFilterCriteriaEntity(List.of(namespace)), List.of(logicalBackup), List.of(externalDatabase), BackupStatus.COMPLETED, null); + backupRepository.save(backup); + + DbaasAdapter dbaasAdapter = Mockito.mock(DbaasAdapter.class); + + when(physicalDatabasesService.getAdapterById(adapterId)).thenReturn(dbaasAdapter); + when(dbaasAdapter.isBackupRestoreSupported()).thenReturn(true); + + // Mock logic of choosing adapter in new/current env + ExternalAdapterRegistrationEntry adapter1 = new ExternalAdapterRegistrationEntry(); + adapter1.setAdapterId(adapterId); + PhysicalDatabase physicalDatabase1 = new PhysicalDatabase(); + physicalDatabase1.setAdapter(adapter1); + physicalDatabase1.setType(postgresType); + physicalDatabase1.setPhysicalDatabaseIdentifier("postgres-dev"); + + when(balancingRulesService.applyBalancingRules(postgresType, mappedNamespace, microserviceName1)) + .thenReturn(physicalDatabase1); + when(physicalDatabasesService.getByAdapterId(adapterId)).thenReturn(physicalDatabase1); + + // Response during the sync restore process + LogicalRestoreAdapterResponse response = LogicalRestoreAdapterResponse.builder() + .status(IN_PROGRESS_STATUS) + .restoreId(logicalRestoreName) + .databases(List.of(LogicalRestoreAdapterResponse.RestoreDatabaseResponse.builder() + .status(IN_PROGRESS_STATUS) + .previousDatabaseName(dbName) + .databaseName(newName) + .duration(1) + .build())) + .build(); + + // Answer to DryRun + when(dbaasAdapter.restoreV2(eq(logicalBackupName), anyBoolean(), any())) + .thenReturn(response); + + Map namespaceMap = Map.of(namespace, mappedNamespace); + Map tenantMap = Map.of(tenantId, mappedTenantId); + + RestoreResponse restoreResponse = dbBackupV2Service.restore(backupName, + getRestoreRequest(restoreName, List.of(namespace), ExternalDatabaseStrategy.INCLUDE, namespaceMap, tenantMap), + true + ); + assertNull(restoreRepository.findById(restoreName)); + assertNotNull(restoreResponse); + assertEquals(restoreName, restoreResponse.getRestoreName()); + assertEquals(backupName, restoreResponse.getBackupName()); + assertEquals(1, restoreResponse.getLogicalRestores().size()); + + LogicalRestoreResponse logicalRestore = restoreResponse.getLogicalRestores().getFirst(); + assertEquals(logicalRestoreName, logicalRestore.getLogicalRestoreName()); + assertEquals(adapterId, logicalRestore.getAdapterId()); + assertEquals(postgresType, logicalRestore.getType()); + assertEquals(1, logicalRestore.getRestoreDatabases().size()); + + RestoreDatabaseResponse restoreDatabase = logicalRestore.getRestoreDatabases().getFirst(); + assertEquals(newName, restoreDatabase.getName()); + assertEquals(1, restoreDatabase.getUsers().size()); + assertEquals(1, restoreDatabase.getSettings().size()); + assertEquals(2, restoreDatabase.getClassifiers().size()); + + ClassifierResponse classifier1 = restoreDatabase.getClassifiers().stream() + .filter(classifier -> ClassifierType.REPLACED == classifier.getType()) + .findAny().orElse(null); + assertNotNull(classifier1); + assertNotNull(classifier1.getClassifier()); + assertTrue( + mappedNamespace.equals(classifier1.getClassifier().get(NAMESPACE)) && + microserviceName1.equals(classifier1.getClassifier().get(MICROSERVICE_NAME)) && + "tenant".equals(classifier1.getClassifier().get("scope")) && + mappedTenantId.equals(classifier1.getClassifier().get(TENANT_ID)) + ); + assertNotNull(classifier1.getClassifierBeforeMapper()); + assertTrue( + namespace.equals(classifier1.getClassifierBeforeMapper().get(NAMESPACE)) && + microserviceName1.equals(classifier1.getClassifierBeforeMapper().get(MICROSERVICE_NAME)) && + "tenant".equals(classifier1.getClassifierBeforeMapper().get("scope")) && + tenantId.equals(classifier1.getClassifierBeforeMapper().get(TENANT_ID)) + ); + + ClassifierResponse classifier2 = restoreDatabase.getClassifiers().stream() + .filter(classifier -> ClassifierType.TRANSIENT_REPLACED == classifier.getType()) + .findAny().orElse(null); + assertNotNull(classifier2); + assertNotNull(classifier2.getClassifier()); + assertTrue( + anotherNamespace.equals(classifier2.getClassifier().get(NAMESPACE)) && + microserviceName1.equals(classifier2.getClassifier().get(MICROSERVICE_NAME)) && + "tenant".equals(classifier2.getClassifier().get("scope")) && + tenantId.equals(classifier2.getClassifier().get(TENANT_ID)) + ); + assertNull(classifier2.getClassifierBeforeMapper()); + assertEquals(1, restoreResponse.getExternalDatabases().size()); + + RestoreExternalDatabaseResponse externalDatabaseResponse = restoreResponse.getExternalDatabases().getFirst(); + assertEquals(externalDbName, externalDatabaseResponse.getName()); + assertEquals(postgresType, externalDatabaseResponse.getType()); + assertEquals(1, externalDatabaseResponse.getClassifiers().size()); + + ClassifierResponse classifier3 = externalDatabaseResponse.getClassifiers().getFirst(); + assertNotNull(classifier3); + assertEquals(ClassifierType.REPLACED, classifier3.getType()); + assertNotNull(classifier3.getClassifier()); + assertTrue( + mappedNamespace.equals(classifier3.getClassifier().get(NAMESPACE)) && + microserviceName2.equals(classifier3.getClassifier().get(MICROSERVICE_NAME)) && + "tenant".equals(classifier3.getClassifier().get("scope")) && + mappedTenantId.equals(classifier3.getClassifier().get(TENANT_ID)) + ); + assertNotNull(classifier3.getClassifierBeforeMapper()); + assertTrue( + namespace.equals(classifier3.getClassifierBeforeMapper().get(NAMESPACE)) && + microserviceName2.equals(classifier3.getClassifierBeforeMapper().get(MICROSERVICE_NAME)) && + "tenant".equals(classifier3.getClassifierBeforeMapper().get("scope")) && + tenantId.equals(classifier3.getClassifierBeforeMapper().get(TENANT_ID)) + ); + } + + @Test + void retryRestore() { + // 1 ExternalDatabase 2 RestoreDatabase (1 FAILED, 1 COMPLETED), 1 logicalRestore (FAILED), 1 restore (FAILED) + // 1 mapping { namespace : mappedNamespace } + // Assert restore COMPLETED, 3 db created + String restoreName = "restoreName"; + String backupName = "backupName"; + String logicalBackupName = "logicalBackupName"; + String logicalRestoreName = "logicalRestoreName"; + + String dbName = "dbName"; + String dbName2 = "dbName2"; + + String newName = "newName"; + String newName2 = "newName2"; + String externalName = "externalName"; + String adapterId = "adapterId"; + + + String namespace = "namespace"; + String mappedNamespace = "mappedNamespace"; + String anotherNamespace = "anotherNamespace"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + + String tenantId = "tenantId"; + String postgresType = "postgresql"; + + BackupExternalDatabase externalDatabase = getBackupExternalDatabase(externalName, postgresType, List.of(getClassifier(namespace, microserviceName3, tenantId))); + BackupDatabase backupDatabase1 = getBackupDatabase(dbName, List.of(getClassifier(namespace, microserviceName1, tenantId)), false, BackupTaskStatus.COMPLETED, "Internal Server Error"); + BackupDatabase backupDatabase2 = getBackupDatabase(dbName2, List.of(getClassifier(anotherNamespace, microserviceName2, null)), false, BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup = getLogicalBackup(logicalBackupName, adapterId, postgresType, List.of(backupDatabase1, backupDatabase2), BackupTaskStatus.COMPLETED, "Internal Server Error"); + Backup backup = getBackup(backupName, ExternalDatabaseStrategy.INCLUDE, getFilterCriteriaEntity(List.of(namespace)), List.of(logicalBackup), List.of(externalDatabase), BackupStatus.COMPLETED, "Internal Server Error"); + backupRepository.save(backup); + + Classifier classifierWrapper1 = getClassifier(ClassifierType.NEW, mappedNamespace, microserviceName1, null, namespace, null); + Classifier classifierWrapper2 = getClassifier(ClassifierType.NEW, null, microserviceName2, null, namespace, null); + Classifier classifierWrapper3 = getClassifier(ClassifierType.NEW, null, microserviceName3, null, namespace, null); + RestoreExternalDatabase restoreExternalDatabase = getRestoreExternalDb(externalName, postgresType, List.of(classifierWrapper3)); + RestoreDatabase restoreDatabase1 = getRestoreDatabase(backupDatabase1, newName, List.of(classifierWrapper1), Map.of(), null, RestoreTaskStatus.FAILED, 1, "Internal Server Error"); + RestoreDatabase restoreDatabase2 = getRestoreDatabase(backupDatabase2, newName2, List.of(classifierWrapper2), Map.of(), null, RestoreTaskStatus.COMPLETED, 1, null); + LogicalRestore logicalRestore = getLogicalRestore(logicalRestoreName, adapterId, postgresType, List.of(restoreDatabase1, restoreDatabase2), RestoreTaskStatus.FAILED, "Internal Server Error"); + Restore restore = getRestore(backup, restoreName, getFilterCriteriaEntity(List.of(namespace)), null, List.of(logicalRestore), ExternalDatabaseStrategy.INCLUDE, List.of(restoreExternalDatabase), RestoreStatus.FAILED, "Internal Server Error"); + + restoreRepository.save(restore); + + DbaasAdapter dbaasAdapter = Mockito.mock(DbaasAdapter.class); + + when(physicalDatabasesService.getAdapterById(adapterId)).thenReturn(dbaasAdapter); + when(dbaasAdapter.isBackupRestoreSupported()).thenReturn(true); + + // Mock logic of choosing adapter in new/current env + ExternalAdapterRegistrationEntry adapter1 = new ExternalAdapterRegistrationEntry(); + adapter1.setAdapterId(adapterId); + PhysicalDatabase physicalDatabase1 = new PhysicalDatabase(); + physicalDatabase1.setAdapter(adapter1); + physicalDatabase1.setType(postgresType); + physicalDatabase1.setPhysicalDatabaseIdentifier("postgres-dev"); + + when(balancingRulesService.applyBalancingRules(postgresType, mappedNamespace, microserviceName1)) + .thenReturn(physicalDatabase1); + when(physicalDatabasesService.getByAdapterId(adapterId)).thenReturn(physicalDatabase1); + + // Response during the sync restore process + LogicalRestoreAdapterResponse response = LogicalRestoreAdapterResponse.builder() + .status(IN_PROGRESS_STATUS) + .restoreId(logicalRestoreName) + .databases(List.of(LogicalRestoreAdapterResponse.RestoreDatabaseResponse.builder() + .status(IN_PROGRESS_STATUS) + .previousDatabaseName(dbName) + .databaseName(newName) + .duration(1) + .build())) + .build(); + + // Same answer to DryRun non-DryRun mode + when(dbaasAdapter.restoreV2(eq(logicalBackupName), anyBoolean(), any())) + .thenReturn(response) + .thenReturn(response); + + // Response during the async restore process + LogicalRestoreAdapterResponse response2 = LogicalRestoreAdapterResponse.builder() + .status(COMPLETED_STATUS) + .restoreId(logicalRestoreName) + .databases(List.of(LogicalRestoreAdapterResponse.RestoreDatabaseResponse.builder() + .status(COMPLETED_STATUS) + .previousDatabaseName(dbName) + .databaseName(newName) + .duration(1) + .build())) + .build(); + + when(dbaasAdapter.trackRestoreV2(eq(logicalRestoreName), any(), any())) + .thenReturn(response2); + + // Mocks to ensure user process + DbResource resource1 = new DbResource(); + resource1.setId(UUID.randomUUID()); + resource1.setKind("kind"); + resource1.setName("name"); + EnsuredUser user1 = new EnsuredUser(); + user1.setConnectionProperties(Map.of( + "key", "value" + )); + user1.setResources(List.of(resource1)); + + when(dbaasAdapter.ensureUser("username", null, newName, "admin")).thenReturn(user1); + + DbResource resource2 = new DbResource(); + resource2.setId(UUID.randomUUID()); + resource2.setKind("kind"); + resource2.setName("name"); + EnsuredUser user2 = new EnsuredUser(); + user2.setConnectionProperties(Map.of( + "key", "value" + )); + user2.setResources(List.of(resource2)); + + when(dbaasAdapter.ensureUser("username", null, newName2, "admin")).thenReturn(user2); + + RestoreResponse restoreResponse = dbBackupV2Service.retryRestore(restoreName); + dbBackupV2Service.checkRestoresAsync(); + + assertEquals(restoreName, restoreResponse.getRestoreName()); + assertEquals(backupName, restoreResponse.getBackupName()); + assertEquals(RestoreStatus.IN_PROGRESS, restoreResponse.getStatus()); + assertEquals(2, restoreResponse.getTotal()); + assertEquals(1, restoreResponse.getCompleted()); + assertEquals(1, restoreResponse.getLogicalRestores().size()); + + LogicalRestoreResponse logicalRestoreResponse = restoreResponse.getLogicalRestores().getFirst(); + assertEquals(logicalRestoreName, logicalRestoreResponse.getLogicalRestoreName()); + assertEquals(adapterId, logicalRestoreResponse.getAdapterId()); + assertEquals(2, logicalRestoreResponse.getRestoreDatabases().size()); + + RestoreDatabaseResponse restoreDatabaseResponse1 = logicalRestoreResponse.getRestoreDatabases().stream() + .filter(db -> newName.equals(db.getName())).findAny().orElse(null); + assertEquals(1, restoreDatabaseResponse1.getClassifiers().size()); + + ClassifierResponse classifierResponse1 = restoreDatabaseResponse1.getClassifiers().getFirst(); + assertEquals(classifierWrapper1.getType(), classifierResponse1.getType()); + assertEquals(classifierWrapper1.getClassifier(), classifierResponse1.getClassifier()); + assertEquals(classifierWrapper1.getClassifierBeforeMapper(), classifierResponse1.getClassifierBeforeMapper()); + assertEquals(RestoreTaskStatus.IN_PROGRESS, restoreDatabaseResponse1.getStatus()); + + RestoreDatabaseResponse restoreDatabaseResponse2 = logicalRestoreResponse.getRestoreDatabases().stream() + .filter(db -> newName2.equals(db.getName())).findAny().orElse(null); + assertEquals(1, restoreDatabaseResponse2.getClassifiers().size()); + + ClassifierResponse classifierResponse2 = restoreDatabaseResponse2.getClassifiers().getFirst(); + assertEquals(classifierWrapper2.getType(), classifierResponse2.getType()); + assertEquals(classifierWrapper2.getClassifier(), classifierResponse2.getClassifier()); + assertEquals(classifierWrapper2.getClassifierBeforeMapper(), classifierResponse2.getClassifierBeforeMapper()); + assertEquals(RestoreTaskStatus.COMPLETED, restoreDatabaseResponse2.getStatus()); + + // Start assert initialized dbs + List databaseRegistries = databaseRegistryDbaasRepository.findAllDatabaseRegistersAnyLogType(); + assertEquals(3, databaseRegistries.size()); + + DatabaseRegistry databaseRegistry1 = databaseRegistries.stream() + .filter(db -> newName.equals(db.getName())) + .findAny().orElse(null); + assert databaseRegistry1 != null; + assertEquals(classifierWrapper1.getClassifier(), databaseRegistry1.getClassifier()); + assertEquals("postgres-dev", databaseRegistry1.getPhysicalDatabaseId()); + + DatabaseRegistry databaseRegistry2 = databaseRegistries.stream() + .filter(db -> newName2.equals(db.getName())) + .findAny().orElse(null); + assert databaseRegistry2 != null; + assertEquals(classifierWrapper2.getClassifierBeforeMapper(), databaseRegistry2.getClassifier()); + assertEquals("postgres-dev", databaseRegistry2.getPhysicalDatabaseId()); + + DatabaseRegistry databaseRegistry3 = databaseRegistries.stream() + .filter(db -> externalName.equals(db.getName())) + .findAny().orElse(null); + assert databaseRegistry3 != null; + assertEquals(classifierWrapper3.getClassifierBeforeMapper(), databaseRegistry3.getClassifier()); + assertNull(databaseRegistry3.getPhysicalDatabaseId()); + + } + + @Test + void getAllDbByFilter_1() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + String postgresqlType = "postgresql"; + String arangoDbType = "arangoDb"; + String cassandraType = "cassandra"; + + String adapterId = "adapterId"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + + Database db1 = getDatabase(adapterId, dbName1, false, false, ""); + Database db2 = getDatabase(adapterId, dbName2, false, false, "cfg"); + Database db3 = getDatabase(adapterId, dbName3, false, false, "cfg"); + Database db4 = getDatabase(adapterId, dbName4, false, false, ""); + Database db5 = getDatabase(adapterId, dbName5, false, false, ""); + + DatabaseRegistry registry1 = getDatabaseRegistry(db1, namespace1, microserviceName1, "", postgresqlType); + DatabaseRegistry registry2 = getDatabaseRegistry(db1, namespace2, microserviceName1, "", postgresqlType); + DatabaseRegistry registry3 = getDatabaseRegistry(db2, namespace2, microserviceName3, "", cassandraType); + DatabaseRegistry registry4 = getDatabaseRegistry(db3, namespace2, microserviceName4, "", cassandraType); + DatabaseRegistry registry5 = getDatabaseRegistry(db4, namespace3, microserviceName5, "", arangoDbType); + DatabaseRegistry registry6 = getDatabaseRegistry(db5, namespace4, microserviceName6, "", arangoDbType); + + Stream.of(registry1, registry2, registry3, registry4, registry5, registry6) + .forEach(databaseRegistryDbaasRepository::saveAnyTypeLogDb); + + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace1, namespace2)); + filter.setMicroserviceName(List.of(microserviceName1, microserviceName4)); + filter.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + filter.setDatabaseKind(List.of(DatabaseKind.TRANSACTIONAL)); + + Filter exclude = new Filter(); + exclude.setNamespace(List.of(namespace2)); + exclude.setMicroserviceName(List.of(microserviceName1)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + filterCriteria.setExclude(List.of(exclude)); + + Map> dbToBackup = dbBackupV2Service.getAllDbByFilter(filterCriteria); + assertNotNull(dbToBackup); + assertEquals(1, dbToBackup.size()); + + dbToBackup.forEach((db, registries) -> { + assertEquals(db1.getId(), db.getId()); + assertEquals(1, registries.size()); + assertEquals(registry1, db.getDatabaseRegistry().getFirst()); + assertEquals(registry1, registries.getFirst()); + }); + } + + @Test + void getAllDbByFilter_2() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + String postgresqlType = "postgresql"; + String arangoDbType = "arangodb"; + String cassandraType = "cassandra"; + String adapterId = "adapterId"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + + Database db1 = getDatabase(adapterId, dbName1, false, false, ""); + Database db2 = getDatabase(adapterId, dbName2, false, false, "cfg"); + Database db3 = getDatabase(adapterId, dbName3, false, false, "cfg"); + Database db4 = getDatabase(adapterId, dbName4, false, false, ""); + Database db5 = getDatabase(adapterId, dbName5, false, false, ""); + + DatabaseRegistry registry1 = getDatabaseRegistry(db1, namespace1, microserviceName1, "", postgresqlType); + DatabaseRegistry registry2 = getDatabaseRegistry(db1, namespace2, microserviceName1, "", postgresqlType); + DatabaseRegistry registry3 = getDatabaseRegistry(db2, namespace2, microserviceName3, "", cassandraType); + DatabaseRegistry registry4 = getDatabaseRegistry(db3, namespace2, microserviceName4, "", cassandraType); + DatabaseRegistry registry5 = getDatabaseRegistry(db4, namespace3, microserviceName5, "", arangoDbType); + DatabaseRegistry registry6 = getDatabaseRegistry(db5, namespace4, microserviceName6, "", arangoDbType); + + Stream.of(registry1, registry2, registry3, registry4, registry5, registry6) + .forEach(databaseRegistryDbaasRepository::saveAnyTypeLogDb); + + Filter filter1 = new Filter(); + filter1.setNamespace(List.of(namespace1)); + filter1.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + + Filter filter2 = new Filter(); + filter2.setNamespace(List.of(namespace2)); + filter2.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + + Filter exclude = new Filter(); + exclude.setMicroserviceName(List.of(microserviceName1)); + + Filter exclude2 = new Filter(); + exclude2.setNamespace(List.of(namespace2)); + exclude2.setMicroserviceName(List.of(microserviceName4)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter1, filter2)); + filterCriteria.setExclude(List.of(exclude, exclude2)); + + Map> dbToBackup = dbBackupV2Service.getAllDbByFilter(filterCriteria); + assertNotNull(dbToBackup); + assertEquals(1, dbToBackup.size()); + + dbToBackup.forEach((db, registries) -> { + assertEquals(db2.getId(), db.getId()); + assertEquals(1, registries.size()); + assertEquals(registry3, registries.getFirst()); + }); + } + + @Test + void getAllDbByFilter_3() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + + String postgresqlType = "postgresql"; + String arangoDbType = "arangodb"; + String cassandraType = "cassandra"; + String adapterId = "adapterId"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + + Database db1 = getDatabase(adapterId, dbName1, false, false, ""); + Database db2 = getDatabase(adapterId, dbName2, false, false, "cfg"); + Database db3 = getDatabase(adapterId, dbName3, false, false, "cfg"); + Database db4 = getDatabase(adapterId, dbName4, false, false, ""); + Database db5 = getDatabase(adapterId, dbName5, false, false, ""); + + DatabaseRegistry registry1 = getDatabaseRegistry(db1, namespace1, microserviceName1, "", postgresqlType); + DatabaseRegistry registry2 = getDatabaseRegistry(db1, namespace2, microserviceName1, "", postgresqlType); + DatabaseRegistry registry3 = getDatabaseRegistry(db2, namespace2, microserviceName2, "", cassandraType); + DatabaseRegistry registry4 = getDatabaseRegistry(db3, namespace3, microserviceName3, "", cassandraType); + DatabaseRegistry registry5 = getDatabaseRegistry(db4, namespace4, microserviceName4, "", arangoDbType); + DatabaseRegistry registry6 = getDatabaseRegistry(db5, namespace4, microserviceName5, "", arangoDbType); + + Stream.of(registry1, registry2, registry3, registry4, registry5, registry6) + .forEach(databaseRegistryDbaasRepository::saveAnyTypeLogDb); + + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace1, namespace2)); + filter.setMicroserviceName(List.of(microserviceName1)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + + Map> filteredDbs = dbBackupV2Service.getAllDbByFilter(filterCriteria); + assertEquals(1, filteredDbs.size()); + + filteredDbs.forEach((db, registries) -> { + if (db.getId() == db1.getId()) { + assertEquals(db1.getId(), db.getId()); + assertEquals(2, registries.size()); + DatabaseRegistry actualRegistry1 = filteredDbs.get(db1).stream() + .filter(r -> namespace1.equals(r.getNamespace())) + .findAny().orElse(null); + + DatabaseRegistry actualRegistry2 = filteredDbs.get(db1).stream() + .filter(r -> namespace2.equals(r.getNamespace())) + .findAny().orElse(null); + assertEquals(registry1, actualRegistry1); + assertEquals(registry2, actualRegistry2); + } else { + assertEquals(db1.getId(), db.getId()); + } + }); + } + + @Test + void getAllDbByFilter_4() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + String postgresqlType = "postgresql"; + String arangoDbType = "arangodb"; + String cassandraType = "cassandra"; + String adapterId = "adapterId"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + + Database db1 = getDatabase(adapterId, dbName1, false, false, ""); + Database db2 = getDatabase(adapterId, dbName2, false, false, "cfg"); + Database db3 = getDatabase(adapterId, dbName3, false, false, "cfg"); + Database db4 = getDatabase(adapterId, dbName4, false, false, ""); + Database db5 = getDatabase(adapterId, dbName5, false, false, ""); + + DatabaseRegistry registry1 = getDatabaseRegistry(db1, namespace1, microserviceName1, "", postgresqlType); + DatabaseRegistry registry2 = getDatabaseRegistry(db1, namespace1, microserviceName2, "", postgresqlType); + DatabaseRegistry registry3 = getDatabaseRegistry(db2, namespace2, microserviceName3, "", cassandraType); + DatabaseRegistry registry4 = getDatabaseRegistry(db3, namespace2, microserviceName4, "", cassandraType); + DatabaseRegistry registry5 = getDatabaseRegistry(db4, namespace3, microserviceName5, "", arangoDbType); + DatabaseRegistry registry6 = getDatabaseRegistry(db5, namespace4, microserviceName6, "", arangoDbType); + + Stream.of(registry1, registry2, registry3, registry4, registry5, registry6) + .forEach(databaseRegistryDbaasRepository::saveAnyTypeLogDb); + + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace1)); + + Filter filter1 = new Filter(); + filter1.setNamespace(List.of(namespace1)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter, filter1)); + + Map> allDbByFilter = dbBackupV2Service.getAllDbByFilter(filterCriteria); + + assertEquals(1, allDbByFilter.size()); + + allDbByFilter.forEach((db, registries) -> { + assertEquals(db1.getId(), db.getId()); + assertEquals(2, registries.size()); + }); + } + + @Test + void getAllDbByFilter_5() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + String postgresqlType = "postgresql"; + String arangoDbType = "arangodb"; + String cassandraType = "cassandra"; + String adapterId = "adapterId"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + + Database db1 = getDatabase(adapterId, dbName1, false, false, ""); + Database db2 = getDatabase(adapterId, dbName2, false, false, "cfg"); + Database db3 = getDatabase(adapterId, dbName3, false, false, "cfg"); + Database db4 = getDatabase(adapterId, dbName4, false, false, ""); + Database db5 = getDatabase(adapterId, dbName5, false, false, ""); + + DatabaseRegistry registry1 = getDatabaseRegistry(db1, namespace1, microserviceName1, "", postgresqlType); + DatabaseRegistry registry2 = getDatabaseRegistry(db1, namespace1, microserviceName2, "", postgresqlType); + DatabaseRegistry registry3 = getDatabaseRegistry(db2, namespace2, microserviceName3, "", cassandraType); + DatabaseRegistry registry4 = getDatabaseRegistry(db3, namespace2, microserviceName4, "", cassandraType); + DatabaseRegistry registry5 = getDatabaseRegistry(db4, namespace3, microserviceName5, "", arangoDbType); + DatabaseRegistry registry6 = getDatabaseRegistry(db5, namespace4, microserviceName6, "", arangoDbType); + + Stream.of(registry1, registry2, registry3, registry4, registry5, registry6) + .forEach(databaseRegistryDbaasRepository::saveAnyTypeLogDb); + + Filter filter = new Filter(); + filter.setDatabaseKind(List.of(DatabaseKind.TRANSACTIONAL, DatabaseKind.CONFIGURATION)); + + Filter exclude = new Filter(); + exclude.setDatabaseKind(List.of(DatabaseKind.CONFIGURATION, DatabaseKind.TRANSACTIONAL, DatabaseKind.TRANSACTIONAL)); //To check distinct method + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + filterCriteria.setExclude(List.of(exclude)); + + assertThrows(DbNotFoundException.class, + () -> dbBackupV2Service.getAllDbByFilter(filterCriteria)); + } + @Test void getAllDbByFilter_whenDatabasesNotFound() { Filter filter = new Filter(); @@ -1591,6 +2251,310 @@ void getAllDbByFilter_whenDatabasesNotFound() { () -> dbBackupV2Service.getAllDbByFilter(filterCriteria)); } + @Test + void getAllDbByFilter_RestorePart_1() { + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + String dbName6 = "db6"; + + String logicalBackupName1 = "lb1"; + String logicalBackupName2 = "db2"; + String logicalBackupName3 = "db3"; + + String adapterId1 = "adpater1"; + String adapterId2 = "adapter2"; + String adapterId3 = "adapter3"; + + String postgresqlType = "postgresql"; + String cassandraType = "cassandra"; + String arangoType = "arangodb"; + + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + BackupDatabase backupDatabase1 = getBackupDatabase(dbName1, List.of(getClassifier(namespace1, microserviceName1, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase2 = getBackupDatabase(dbName2, List.of(getClassifier(namespace1, microserviceName2, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase3 = getBackupDatabase(dbName3, List.of(getClassifier(namespace2, microserviceName3, null)), true, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase4 = getBackupDatabase(dbName4, List.of(getClassifier(namespace2, microserviceName4, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase5 = getBackupDatabase(dbName5, List.of(getClassifier(namespace3, microserviceName5, null)), true, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase6 = getBackupDatabase(dbName6, List.of(getClassifier(namespace4, microserviceName6, null)), false, BackupTaskStatus.COMPLETED, null); + + LogicalBackup logicalBackup1 = getLogicalBackup(logicalBackupName1, adapterId1, postgresqlType, List.of(backupDatabase1, backupDatabase2), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup2 = getLogicalBackup(logicalBackupName2, adapterId2, cassandraType, List.of(backupDatabase3, backupDatabase4), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup3 = getLogicalBackup(logicalBackupName3, adapterId3, arangoType, List.of(backupDatabase5, backupDatabase6), BackupTaskStatus.COMPLETED, null); + + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace1, namespace2)); + filter.setMicroserviceName(List.of(microserviceName1, microserviceName4)); + filter.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + filter.setDatabaseKind(List.of(DatabaseKind.TRANSACTIONAL)); + + Filter exclude = new Filter(); + exclude.setMicroserviceName(List.of(microserviceName4)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + filterCriteria.setExclude(List.of(exclude)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3, backupDatabase4, backupDatabase5, backupDatabase6); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(1, filteredDatabases.size()); + + BackupDatabaseDelegate backupDatabaseDelegate = filteredDatabases.getFirst(); + + assertEquals(backupDatabaseDelegate.backupDatabase(), backupDatabase1); + assertEquals(backupDatabaseDelegate.classifiers().getFirst().getClassifierBeforeMapper(), backupDatabase1.getClassifiers().getFirst()); + } + + @Test + void getAllDbByFilter_RestorePart_2() { + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + String dbName4 = "db4"; + String dbName5 = "db5"; + String dbName6 = "db6"; + + String logicalBackupName1 = "lb1"; + String logicalBackupName2 = "db2"; + String logicalBackupName3 = "db3"; + + String adapterId1 = "adpater1"; + String adapterId2 = "adapter2"; + String adapterId3 = "adapter3"; + + String postgresqlType = "postgresql"; + String cassandraType = "cassandra"; + String arangoType = "arangodb"; + + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + String namespace4 = "namespace4"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + String microserviceName4 = "microserviceName4"; + String microserviceName5 = "microserviceName5"; + String microserviceName6 = "microserviceName6"; + + BackupDatabase backupDatabase1 = getBackupDatabase(dbName1, List.of(getClassifier(namespace1, microserviceName1, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase2 = getBackupDatabase(dbName2, List.of(getClassifier(namespace1, microserviceName2, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase3 = getBackupDatabase(dbName3, List.of(getClassifier(namespace2, microserviceName3, null)), true, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase4 = getBackupDatabase(dbName4, List.of(getClassifier(namespace2, microserviceName4, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase5 = getBackupDatabase(dbName5, List.of(getClassifier(namespace3, microserviceName5, null)), true, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase6 = getBackupDatabase(dbName6, List.of(getClassifier(namespace4, microserviceName6, null)), false, BackupTaskStatus.COMPLETED, null); + + LogicalBackup logicalBackup1 = getLogicalBackup(logicalBackupName1, adapterId1, postgresqlType, List.of(backupDatabase1, backupDatabase2), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup2 = getLogicalBackup(logicalBackupName2, adapterId2, cassandraType, List.of(backupDatabase3, backupDatabase4), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup3 = getLogicalBackup(logicalBackupName3, adapterId3, arangoType, List.of(backupDatabase5, backupDatabase6), BackupTaskStatus.COMPLETED, null); + + Filter filter1 = new Filter(); + filter1.setNamespace(List.of(namespace1)); + filter1.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + filter1.setDatabaseKind(List.of(DatabaseKind.TRANSACTIONAL)); + + Filter filter2 = new Filter(); + filter2.setNamespace(List.of(namespace2)); + filter2.setMicroserviceName(List.of(microserviceName3)); + filter2.setDatabaseType(List.of(DatabaseType.POSTGRESQL, DatabaseType.CASSANDRA)); + + Filter exclude = new Filter(); + exclude.setDatabaseType(List.of(DatabaseType.POSTGRESQL)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter1, filter2)); + filterCriteria.setExclude(List.of(exclude)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3, backupDatabase4, backupDatabase5, backupDatabase6); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(1, filteredDatabases.size()); + + BackupDatabaseDelegate backupDatabaseDelegate = filteredDatabases.getFirst(); + + assertEquals(backupDatabaseDelegate.backupDatabase(), backupDatabase3); + assertEquals(backupDatabaseDelegate.classifiers().getFirst().getClassifierBeforeMapper(), backupDatabase3.getClassifiers().getFirst()); + } + + @Test + void getAllDbByFilter_RestorePart_3() { + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + + String logicalBackupName1 = "lb1"; + String logicalBackupName2 = "db2"; + + String adapterId1 = "adpater1"; + String adapterId2 = "adapter2"; + + String postgresqlType = "postgresql"; + String cassandraType = "cassandra"; + + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + + BackupDatabase backupDatabase1 = getBackupDatabase(dbName1, List.of(getClassifier(namespace1, microserviceName1, null), getClassifier(namespace2, microserviceName1, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase2 = getBackupDatabase(dbName2, List.of(getClassifier(namespace2, microserviceName2, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase3 = getBackupDatabase(dbName3, List.of(getClassifier(namespace3, microserviceName3, null)), true, BackupTaskStatus.COMPLETED, null); + + LogicalBackup logicalBackup1 = getLogicalBackup(logicalBackupName1, adapterId1, postgresqlType, List.of(backupDatabase1, backupDatabase2), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup2 = getLogicalBackup(logicalBackupName2, adapterId2, cassandraType, List.of(backupDatabase3), BackupTaskStatus.COMPLETED, null); + + Filter filter1 = new Filter(); + filter1.setNamespace(List.of(namespace1)); + + Filter filter2 = new Filter(); + filter2.setNamespace(List.of(namespace2)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter1, filter2)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(2, filteredDatabases.size()); + + BackupDatabaseDelegate backupDatabaseDelegate1 = filteredDatabases.stream() + .filter(db -> dbName1.equals(db.backupDatabase().getName())) + .findAny().orElse(null); + assertNotNull(backupDatabaseDelegate1); + assertEquals(backupDatabaseDelegate1.backupDatabase(), backupDatabase1); + assertEquals(2, backupDatabaseDelegate1.classifiers().size()); + + SortedMap classifier1 = backupDatabase1.getClassifiers().stream() + .filter(classifier -> namespace1.equals(classifier.get(NAMESPACE))) + .findAny().orElse(null); + SortedMap classifier2 = backupDatabase1.getClassifiers().stream() + .filter(classifier -> namespace2.equals(classifier.get(NAMESPACE))) + .findAny().orElse(null); + assertNotNull(classifier1); + assertNotNull(classifier2); + + BackupDatabaseDelegate backupDatabaseDelegate2 = filteredDatabases.stream() + .filter(db -> dbName2.equals(db.backupDatabase().getName())) + .findAny().orElse(null); + assertNotNull(backupDatabaseDelegate2); + assertEquals(backupDatabaseDelegate2.backupDatabase(), backupDatabase2); + assertEquals(1, backupDatabaseDelegate2.classifiers().size()); + assertEquals(backupDatabaseDelegate2.classifiers().getFirst().getClassifierBeforeMapper(), backupDatabase2.getClassifiers().getFirst()); + } + + @Test + void getAllDbByFilter_RestorePart_4() { + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + + String logicalBackupName1 = "lb1"; + String logicalBackupName2 = "db2"; + + String adapterId1 = "adpater1"; + String adapterId2 = "adapter2"; + + String postgresqlType = "postgresql"; + String cassandraType = "cassandra"; + + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + + BackupDatabase backupDatabase1 = getBackupDatabase(dbName1, List.of(getClassifier(namespace1, microserviceName1, null), getClassifier(namespace2, microserviceName1, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase2 = getBackupDatabase(dbName2, List.of(getClassifier(namespace2, microserviceName2, null)), false, BackupTaskStatus.COMPLETED, null); + BackupDatabase backupDatabase3 = getBackupDatabase(dbName3, List.of(getClassifier(namespace3, microserviceName3, null)), true, BackupTaskStatus.COMPLETED, null); + + LogicalBackup logicalBackup1 = getLogicalBackup(logicalBackupName1, adapterId1, postgresqlType, List.of(backupDatabase1, backupDatabase2), BackupTaskStatus.COMPLETED, null); + LogicalBackup logicalBackup2 = getLogicalBackup(logicalBackupName2, adapterId2, cassandraType, List.of(backupDatabase3), BackupTaskStatus.COMPLETED, null); + + Filter filter1 = new Filter(); + filter1.setNamespace(List.of(namespace1)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter1)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(1, filteredDatabases.size()); + + BackupDatabaseDelegate backupDatabaseDelegate1 = filteredDatabases.stream() + .filter(db -> dbName1.equals(db.backupDatabase().getName())) + .findAny().orElse(null); + assertNotNull(backupDatabaseDelegate1); + assertEquals(backupDatabaseDelegate1.backupDatabase(), backupDatabase1); + assertEquals(1, backupDatabaseDelegate1.classifiers().size()); + + SortedMap classifier1 = backupDatabase1.getClassifiers().stream() + .filter(classifier -> namespace1.equals(classifier.get(NAMESPACE))) + .findAny().orElse(null); + assertNotNull(classifier1); + } + + @Test + void validateAndFilterExternalDb_testFiltering() { + String namespace1 = "namespace1"; + String namespace2 = "namespace2"; + String namespace3 = "namespace3"; + + String microserviceName1 = "microserviceName1"; + String microserviceName2 = "microserviceName2"; + String microserviceName3 = "microserviceName3"; + + String dbName1 = "db1"; + String dbName2 = "db2"; + String dbName3 = "db3"; + + String postgresqlType = "postgresql"; + SortedMap classifier = getClassifier(namespace1, microserviceName1, null); + + BackupExternalDatabase externalDatabase1 = getBackupExternalDatabase(dbName1, postgresqlType, List.of(classifier)); + BackupExternalDatabase externalDatabase2 = getBackupExternalDatabase(dbName2, postgresqlType, List.of(getClassifier(namespace2, microserviceName2, null))); + BackupExternalDatabase externalDatabase3 = getBackupExternalDatabase(dbName3, postgresqlType, List.of(getClassifier(namespace3, microserviceName3, null))); + + Filter filter = new Filter(); + filter.setNamespace(List.of(namespace1, namespace2)); + + Filter exclude = new Filter(); + exclude.setMicroserviceName(List.of(microserviceName2)); + + FilterCriteria filterCriteria = new FilterCriteria(); + filterCriteria.setFilter(List.of(filter)); + filterCriteria.setExclude(List.of(exclude)); + + List restoreExternalDatabases = dbBackupV2Service.validateAndFilterExternalDb(List.of(externalDatabase1, externalDatabase2, externalDatabase3), ExternalDatabaseStrategy.INCLUDE, filterCriteria); + assertEquals(1, restoreExternalDatabases.size()); + + RestoreExternalDatabase externalDb = restoreExternalDatabases.getFirst(); + assertEquals(dbName1, externalDb.getName()); + assertEquals(postgresqlType, externalDb.getType()); + assertEquals(1, externalDb.getClassifiers().size()); + assertEquals(classifier, externalDb.getClassifiers().getFirst().getClassifierBeforeMapper()); + } + @Test void validateAndFilterDatabasesForBackup_ExternalDatabaseStrategyInclude() { String namespace = "namespace"; @@ -2143,6 +3107,7 @@ void uploadBackupMetadata_restoreDeletedBackup_digestMismatch() { BackupResponse backupResponse = getBackupResponse(backupName, namespace); backupResponse.setDigest(anotherDigest); + IntegrityViolationException ex = assertThrows(IntegrityViolationException.class, () -> dbBackupV2Service.uploadBackupMetadata(backupResponse)); assertEquals( @@ -2275,6 +3240,37 @@ private SortedMap getClassifier(String namespace, String microse return classifier; } + private Classifier getClassifier(ClassifierType classifierType, String namespace, String microserviceName, String tenantId, String namespaceBeforeMap, String tenantIdBeforeMap) { + assertFalse(namespaceBeforeMap.isBlank()); + + Classifier classifierWrapper = new Classifier(); + classifierWrapper.setType(classifierType); + + if(namespace != null && !namespace.isBlank()) { + SortedMap classifier = new TreeMap<>(); + classifier.put("namespace", namespace); + classifier.put("microserviceName", microserviceName); + if (tenantId != null && !tenantId.isBlank()) { + classifier.put("tenantId", tenantId); + classifier.put("scope", "tenant"); + } else + classifier.put("scope", "service"); + classifierWrapper.setClassifier(classifier); + } + + SortedMap classifierBeforeMapping = new TreeMap<>(); + classifierBeforeMapping.put("namespace", namespaceBeforeMap); + classifierBeforeMapping.put("microserviceName", microserviceName); + if (tenantId != null && !tenantId.isBlank()) { + classifierBeforeMapping.put("tenantId", tenantIdBeforeMap); + classifierBeforeMapping.put("scope", "tenant"); + } else + classifierBeforeMapping.put("scope", "service"); + + classifierWrapper.setClassifierBeforeMapper(classifierBeforeMapping); + return classifierWrapper; + } + private BackupExternalDatabase getBackupExternalDatabase(String name, String type, List> classifiers) { return BackupExternalDatabase.builder() .name(name) @@ -2420,6 +3416,9 @@ private Restore getRestore(String restoreName, String namespace) { SortedMap classifier = new TreeMap<>(); classifier.put("namespace", namespace); + Classifier classifierMapper = new Classifier(); + classifierMapper.setClassifierBeforeMapper(classifier); + List restoreDatabases = new ArrayList<>(); for (int i = 0; i < 3; i++) { RestoreDatabase restoreDatabase = RestoreDatabase.builder() @@ -2428,7 +3427,7 @@ private Restore getRestore(String restoreName, String namespace) { new RestoreDatabase.User("username", "admin") )) .path("path") - .classifiers(List.of(classifier)) + .classifiers(List.of(classifierMapper)) .build(); restoreDatabases.add(restoreDatabase); } @@ -2510,7 +3509,7 @@ private Map> getDatabase(Map db private RestoreDatabase getRestoreDatabase(BackupDatabase backupDatabase, String dbName, - List> classifiers, + List classifiers, Map settings, String bgVersion, RestoreTaskStatus status, @@ -2576,6 +3575,18 @@ private Restore getRestore(Backup backup, return restore; } + private RestoreExternalDatabase getRestoreExternalDb( + String name, + String type, + List classifiers + ) { + return RestoreExternalDatabase.builder() + .name(name) + .type(type) + .classifiers(classifiers) + .build(); + } + private BackupRequest getBackupRequest(String backupName, List namespaces, ExternalDatabaseStrategy strategy, diff --git a/docs/OpenAPI.json b/docs/OpenAPI.json index f45181c5..bef8a387 100644 --- a/docs/OpenAPI.json +++ b/docs/OpenAPI.json @@ -338,13 +338,6 @@ }, "description": "Apply the filter to the remaining databases" }, - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Include databases that match any of the filters in the list" - }, "exclude": { "type": "array", "items": { @@ -452,13 +445,6 @@ }, "description": "Apply the filter to the remaining databases" }, - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Include databases that match any of the filters in the list" - }, "exclude": { "type": "array", "items": { @@ -654,6 +640,30 @@ } } }, + "ClassifierResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/ClassifierType" + }, + "classifier": { + "type": "object", + "additionalProperties": {} + }, + "classifierBeforeMapper": { + "type": "object", + "additionalProperties": {} + } + } + }, + "ClassifierType": { + "type": "string", + "enum": [ + "NEW", + "REPLACED", + "TRANSIENT_REPLACED" + ] + }, "ClassifierWithRolesRequest": { "type": "object", "required": [ @@ -1802,13 +1812,6 @@ }, "description": "Apply the filter to the remaining databases" }, - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Include databases that match any of the filters in the list" - }, "exclude": { "type": "array", "items": { @@ -3099,8 +3102,7 @@ ] ], "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/ClassifierResponse" }, "description": "List of database classifiers. Each classifier is a sorted map of attributes." }, @@ -3207,8 +3209,7 @@ ] ], "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/ClassifierResponse" }, "description": "List of database classifiers. Each classifier is a sorted map of attributes." } @@ -3252,6 +3253,7 @@ "examples": [ "restore-before-prod-update-20251203T1020-4t6S" ], + "pattern": "\\S", "description": "Unique identifier of the restore" }, "storageName": { @@ -3259,6 +3261,7 @@ "examples": [ "s3-backend" ], + "pattern": "\\S", "description": "Name of the storage backend containing the restore" }, "blobPath": { @@ -3266,6 +3269,7 @@ "examples": [ "/backups" ], + "pattern": "\\S", "description": "Path to the restore file in the storage" }, "filterCriteria": { @@ -3282,13 +3286,6 @@ }, "description": "Apply the filter to the remaining databases" }, - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Include databases that match any of the filters in the list" - }, "exclude": { "type": "array", "items": { @@ -3412,13 +3409,6 @@ }, "description": "Apply the filter to the remaining databases" }, - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Include databases that match any of the filters in the list" - }, "exclude": { "type": "array", "items": { @@ -3495,22 +3485,6 @@ ], "description": "Aggregated error messages during restore operation" }, - "duration": { - "type": "integer", - "format": "int64", - "examples": [ - 1200 - ], - "description": "Aggregated duration of databases" - }, - "attemptCount": { - "type": "integer", - "format": "int32", - "examples": [ - 1 - ], - "description": "Total number of adapter requests" - }, "logicalRestores": { "type": "array", "items": { @@ -4594,14 +4568,14 @@ ], "requestBody": { "description": "Restore request", - "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RestoreRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -4989,16 +4963,6 @@ } ], "responses": { - "200": { - "description": "Restore operation retried successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RestoreResponse" - } - } - } - }, "202": { "description": "Restore retry accepted and is being processed", "content": {