diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/error/OperationAlreadyRunningExceptionMapper.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/error/OperationAlreadyRunningExceptionMapper.java new file mode 100644 index 00000000..b8453a2f --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/error/OperationAlreadyRunningExceptionMapper.java @@ -0,0 +1,22 @@ +package com.netcracker.cloud.dbaas.controller.error; + +import com.netcracker.cloud.dbaas.exceptions.OperationAlreadyRunningException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import static com.netcracker.cloud.dbaas.controller.error.Utils.buildDefaultResponse; + +@Provider +public class OperationAlreadyRunningExceptionMapper implements ExceptionMapper { + + @Context + UriInfo uriInfo; + + @Override + public Response toResponse(OperationAlreadyRunningException e) { + return buildDefaultResponse(uriInfo, e, Response.Status.CONFLICT); + } +} 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..8a1a7f12 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 @@ -5,8 +5,10 @@ import com.netcracker.cloud.dbaas.dto.backupV2.*; import com.netcracker.cloud.dbaas.enums.BackupStatus; import com.netcracker.cloud.dbaas.enums.RestoreStatus; +import com.netcracker.cloud.dbaas.exceptions.ForbiddenDeleteOperationException; import com.netcracker.cloud.dbaas.exceptions.IntegrityViolationException; import com.netcracker.cloud.dbaas.service.DbBackupV2Service; +import com.netcracker.cloud.dbaas.service.DbaaSHelper; import com.netcracker.cloud.dbaas.utils.DigestUtil; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; @@ -41,15 +43,17 @@ public class DatabaseBackupV2Controller { private final DbBackupV2Service dbBackupV2Service; + private final DbaaSHelper dbaaSHelper; @Inject - public DatabaseBackupV2Controller(DbBackupV2Service dbBackupV2Service) { + public DatabaseBackupV2Controller(DbBackupV2Service dbBackupV2Service, DbaaSHelper dbaaSHelper) { this.dbBackupV2Service = dbBackupV2Service; + this.dbaaSHelper = dbaaSHelper; } @Operation(summary = "Initiate database backup", description = "Starts an asynchronous backup operation for the specified databases." - + " Returns immediately with a backup identifier that can be used to track progress.") + + " Returns immediately with a backup name that can be used to track progress.") @APIResponses({ @APIResponse(responseCode = "200", description = "Backup operation completed successfully", content = @Content(schema = @Schema(implementation = BackupResponse.class))), @@ -63,17 +67,16 @@ public DatabaseBackupV2Controller(DbBackupV2Service dbBackupV2Service) { content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "409", description = "The request could not be completed due to a conflict with the current state of the resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), - @APIResponse(responseCode = "422", description = "The request was accepted, but the server could`t process due to incompatible resource", + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", - content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), - @APIResponse(responseCode = "501", description = "The server does not support the functionality required to fulfill the request", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/backup") @POST - public Response initiateBackup(@RequestBody(description = "Backup request", required = true) @Valid BackupRequest backupRequest, + public Response initiateBackup(@RequestBody(description = "Backup request") @Valid BackupRequest backupRequest, @QueryParam("dryRun") @DefaultValue("false") boolean dryRun) { + log.info("Request to start backup with backup request {}, with dryRun mode {}", backupRequest, dryRun); BackupResponse response = dbBackupV2Service.backup(backupRequest, dryRun); BackupStatus status = response.getStatus(); if (status == BackupStatus.COMPLETED || status == BackupStatus.FAILED) @@ -85,6 +88,8 @@ public Response initiateBackup(@RequestBody(description = "Backup request", requ @APIResponses({ @APIResponse(responseCode = "200", description = "Backup details retrieved successfully", content = @Content(schema = @Schema(implementation = BackupResponse.class))), + @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), @APIResponse(responseCode = "404", description = "The requested resource could not be found", @@ -94,7 +99,10 @@ public Response initiateBackup(@RequestBody(description = "Backup request", requ }) @Path("/backup/{backupName}") @GET - public Response getBackup(@Parameter(description = "Unique identifier of the backup", required = true) @PathParam("backupName") String backupName) { + public Response getBackup(@Parameter(description = "Unique name of the backup", required = true) + @PathParam("backupName") + @NotBlank String backupName) { + log.info("Request to get backup {}", backupName); return Response.ok(dbBackupV2Service.getBackup(backupName)).build(); } @@ -102,18 +110,21 @@ public Response getBackup(@Parameter(description = "Unique identifier of the bac @APIResponses({ @APIResponse(responseCode = "202", description = "Backup delete initialized successfully"), @APIResponse(responseCode = "204", description = "Backup deleted successfully"), + @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), - @APIResponse(responseCode = "404", description = "The requested resource could not be found", + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/backup/{backupName}") @DELETE - public Response deleteBackup(@Parameter(description = "Unique identifier of the backup", required = true) - @PathParam("backupName") String backupName, + public Response deleteBackup(@Parameter(description = "Unique name of the backup", required = true) + @PathParam("backupName") @NotBlank String backupName, @QueryParam("force") @DefaultValue("false") boolean force) { + log.info("Request to delete backup {} with flag force {}", backupName, force); dbBackupV2Service.deleteBackup(backupName, force); if (force) return Response.accepted().build(); @@ -124,6 +135,8 @@ public Response deleteBackup(@Parameter(description = "Unique identifier of the @APIResponses({ @APIResponse(responseCode = "200", description = "Backup status retrieved successfully", content = @Content(schema = @Schema(implementation = BackupStatusResponse.class))), + @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), @APIResponse(responseCode = "404", description = "The requested resource could not be found", @@ -133,9 +146,10 @@ public Response deleteBackup(@Parameter(description = "Unique identifier of the }) @Path("/backup/{backupName}/status") @GET - public Response getBackupStatus(@Parameter(description = "Unique identifier of the backup", required = true) + public Response getBackupStatus(@Parameter(description = "Unique name of the backup", required = true) @PathParam("backupName") @NotBlank String backupName) { + log.info("Request to get backup status {}", backupName); return Response.ok(dbBackupV2Service.getCurrentStatus(backupName)).build(); } @@ -155,20 +169,23 @@ public Response getBackupStatus(@Parameter(description = "Unique identifier of t }, content = @Content(schema = @Schema(implementation = BackupResponse.class)) ), + @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), @APIResponse(responseCode = "404", description = "The requested resource could not be found", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), - @APIResponse(responseCode = "422", description = "The request was accepted, but the server could`t process due to incompatible resource", + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/backup/{backupName}/metadata") @GET - public Response getBackupMetadata(@Parameter(description = "Unique identifier of the backup", required = true) + public Response getBackupMetadata(@Parameter(description = "Unique name of the backup", required = true) @PathParam("backupName") @NotBlank String backupName) { + log.info("Request to get backup metadata {}", backupName); BackupResponse response = dbBackupV2Service.getBackupMetadata(backupName); String digestHeader = DigestUtil.calculateDigest(response); return Response.ok(response) @@ -176,7 +193,7 @@ public Response getBackupMetadata(@Parameter(description = "Unique identifier of .build(); } - @Operation(summary = "Upload backup metadata", description = "Metadata upload done") + @Operation(summary = "Upload backup metadata", description = "Upload backup metadata") @APIResponses({ @APIResponse(responseCode = "200", description = "Backup metadata uploaded successfully"), @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", @@ -193,17 +210,19 @@ public Response getBackupMetadata(@Parameter(description = "Unique identifier of public Response uploadMetadata( @Parameter( name = "Digest", - description = "Digest header in format: sha-256=", + description = "Digest header in format: SHA-256=", required = true, in = ParameterIn.HEADER, schema = @Schema( type = SchemaType.STRING, examples = { - "sha-256=nOJRJg..." + "SHA-256=nOJRJg..." })) @HeaderParam("Digest") @NotNull String digestHeader, - @RequestBody(description = "Backup metadata", required = true) @Valid BackupResponse backupResponse + @RequestBody(description = "Backup metadata") @Valid BackupResponse backupResponse ) { + log.info("Request to upload backup metadata {}", backupResponse); + log.debug("Backup digest {}", digestHeader); String calculatedDigest = DigestUtil.calculateDigest(backupResponse); if (!calculatedDigest.equals(digestHeader)) throw new IntegrityViolationException( @@ -215,7 +234,7 @@ public Response uploadMetadata( } @Operation(summary = "Restore from backup", description = "Initiate a database restore operation from an existing backup." + - "This operation is asynchronous and returns immediately with a restore identifier that can be used to track progress." + + "This operation is asynchronous and returns immediately with a restore name that can be used to track progress." + "Operation is not idempotent") @APIResponses({ @APIResponse(responseCode = "200", description = "Restore operation completed successfully", @@ -230,21 +249,44 @@ public Response uploadMetadata( content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "409", description = "The request could not be completed due to a conflict with the current state of the resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), - @APIResponse(responseCode = "422", description = "The request was accepted, but the server could`t process due to incompatible resource", + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", - content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), - @APIResponse(responseCode = "501", description = "The server does not support the functionality required to fulfill the request", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/backup/{backupName}/restore") @POST - public Response restoreBackup(@Parameter(description = "Unique identifier of the backup", required = true) + public Response restoreBackup(@Parameter(description = "Unique name 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); + log.info("Request to restore backup {}, restore request {}, dryRun mode {}", backupName, restoreRequest, dryRun); + return restore(backupName, restoreRequest, dryRun, false); + } + + @Operation( + summary = "Restore from backup with parallel execution allowed", + description = "Only for internal usage", + hidden = true + ) + @Path("/backup/{backupName}/restore/allowParallel") + @POST + public Response restoreBackupAllowParallel(@Parameter(description = "Unique name of the backup", required = true) + @PathParam("backupName") @NotBlank String backupName, + @RequestBody(description = "Restore request") + @Valid RestoreRequest restoreRequest, + @QueryParam("dryRun") @DefaultValue("false") boolean dryRun) { + log.info("Request to restore backup with parallel execution allowed," + + " backup name {}, restore request {}, dryRun mode {}", backupName, restoreRequest, dryRun); + if (dbaaSHelper.isProductionMode()) { + throw new ForbiddenDeleteOperationException(); + } + return restore(backupName, restoreRequest, dryRun, true); + } + + private Response restore(String backupName, RestoreRequest restoreRequest, boolean dryRun, boolean allowParallel) { + RestoreResponse response = dbBackupV2Service.restore(backupName, restoreRequest, dryRun, allowParallel); RestoreStatus status = response.getStatus(); if (status == RestoreStatus.COMPLETED || status == RestoreStatus.FAILED) return Response.ok(response).build(); @@ -255,6 +297,8 @@ public Response restoreBackup(@Parameter(description = "Unique identifier of the @APIResponses({ @APIResponse(responseCode = "200", description = "Restore details retrieved successfully", content = @Content(schema = @Schema(implementation = RestoreResponse.class))), + @APIResponse(responseCode = "400", description = "The request was invalid or cannot be served", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), @APIResponse(responseCode = "404", description = "The requested resource could not be found", @@ -264,9 +308,10 @@ public Response restoreBackup(@Parameter(description = "Unique identifier of the }) @Path("/restore/{restoreName}") @GET - public Response getRestore(@Parameter(description = "Unique identifier of the restore operation", required = true) + public Response getRestore(@Parameter(description = "Unique name of the restore operation", required = true) @PathParam("restoreName") @NotBlank String restoreName) { + log.info("Request to get restore {}", restoreName); return Response.ok(dbBackupV2Service.getRestore(restoreName)).build(); } @@ -275,14 +320,17 @@ public Response getRestore(@Parameter(description = "Unique identifier of the re @APIResponse(responseCode = "204", description = "Restore operation deleted successfully"), @APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), - @APIResponse(responseCode = "404", description = "The requested resource could not be found", + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/restore/{restoreName}") @DELETE - public Response deleteRestore(@Parameter(description = "Unique identifier of the restore operation", required = true) @PathParam("restoreName") String restoreName) { + public Response deleteRestore(@Parameter(description = "Unique name of the restore operation", required = true) + @PathParam("restoreName") + @NotBlank String restoreName) { + log.info("Request to delete restore {}", restoreName); dbBackupV2Service.deleteRestore(restoreName); return Response.noContent().build(); } @@ -300,31 +348,69 @@ public Response deleteRestore(@Parameter(description = "Unique identifier of the }) @Path("/restore/{restoreName}/status") @GET - public Response getRestoreStatus(@Parameter(description = "Unique identifier of the restore operation", required = true) + public Response getRestoreStatus(@Parameter(description = "Unique name of the restore operation", required = true) @PathParam("restoreName") @NotBlank String restoreName) { + log.info("Request to get restore status {}", restoreName); return Response.ok(dbBackupV2Service.getRestoreStatus(restoreName)).build(); } @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"), @APIResponse(responseCode = "403", description = "The request was valid, but the server is refusing action"), @APIResponse(responseCode = "404", description = "The requested resource could not be found", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), + @APIResponse(responseCode = "409", description = "The request could not be completed due to a conflict with the current state of the resource", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), + @APIResponse(responseCode = "422", description = "The request was accepted, but the server couldn't process due to incompatible resource", + content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))), @APIResponse(responseCode = "500", description = "An unexpected error occurred on the server", content = @Content(schema = @Schema(implementation = TmfErrorResponse.class))) }) @Path("/restore/{restoreName}/retry") @POST - public Response retryRestore(@Parameter(description = "Unique identifier of the restore operation", required = true) + public Response retryRestore(@Parameter(description = "Unique name of the restore operation", required = true) @PathParam("restoreName") - String restoreName) { - dbBackupV2Service.retryRestore(restoreName); - return Response.ok().build(); + @NotBlank String restoreName) { + log.info("Request to retry restore {}", restoreName); + return Response.accepted(dbBackupV2Service.retryRestore(restoreName, false)).build(); + } + + @Operation( + summary = "Retry restore with parallel execution allowed", + description = "Only for internal usage", + hidden = true + ) + @Path("/restore/{restoreName}/retry/allowParallel") + @POST + public Response retryRestoreAllowParallel(@Parameter(description = "Unique name of the restore operation", required = true) + @PathParam("restoreName") + @NotBlank String restoreName) { + log.info("Request to retry restore with parallel execution alllowed, restore name {}", restoreName); + if (dbaaSHelper.isProductionMode()) { + throw new ForbiddenDeleteOperationException(); + } + return Response.accepted(dbBackupV2Service.retryRestore(restoreName, true)).build(); + } + + @Operation(summary = "Remove backup", + description = "Deleting a backup entirely from DB by the specified backup name. Only for internal usage.", + hidden = true + ) + @Path("/backup/{backupName}/forceDelete") + @DELETE + public Response deleteBackupFromDb(@Parameter(description = "Unique name of the backup operation", required = true) + @PathParam("backupName") + @NotBlank String backupName + ) { + log.info("Request to delete backup from db, backup name {}", backupName); + if (dbaaSHelper.isProductionMode()) { + throw new ForbiddenDeleteOperationException(); + } + dbBackupV2Service.deleteBackupFromDb(backupName); + return Response.noContent().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/BackupDatabaseResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupDatabaseResponse.java index 77465a70..5c2eed6b 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupDatabaseResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupDatabaseResponse.java @@ -11,11 +11,18 @@ import java.util.List; import java.util.Map; import java.util.SortedMap; +import java.util.UUID; @Data @AllArgsConstructor @Schema(description = "Logical database backup details") public class BackupDatabaseResponse { + @Schema( + description = "Identifier of the backup database", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema( description = "Name of the database", examples = { @@ -31,12 +38,12 @@ public class BackupDatabaseResponse { private List> classifiers; @Schema( description = "Database settings as a key-value map", - examples = "{\"key\":value, \"key\":value}" + examples = "{\"key\": \"value\", \"key\": \"value\"}" ) private Map settings; @Schema( description = "List of database users", - examples = "[{\"name\":\"username\",\"role\":\"admin\"}" + examples = "[{\"name\":\"username\",\"role\":\"admin\"}]" ) private List users; @Schema( diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupExternalDatabaseResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupExternalDatabaseResponse.java index 932881ed..b08d5ef9 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupExternalDatabaseResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/BackupExternalDatabaseResponse.java @@ -6,11 +6,18 @@ import java.util.List; import java.util.SortedMap; +import java.util.UUID; @Data @NoArgsConstructor @Schema(description = "External database details") public class BackupExternalDatabaseResponse { + @Schema( + description = "Identifier of the external backup database", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema(description = "Name of the external database", examples = "mydb", required = true) private String name; @Schema( 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..df21a328 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,12 +1,11 @@ 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; import jakarta.validation.groups.ConvertGroup; -import jakarta.validation.groups.Default; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -17,7 +16,7 @@ public class BackupRequest { @NotBlank @Schema( - description = "Unique identifier of the backup", + description = "Unique name of the backup", examples = { "before-prod-update-20251013T1345-G5s8" }, @@ -45,7 +44,7 @@ public class BackupRequest { ) @Valid @NotNull - @ConvertGroup(from = Default.class, to = BackupGroup.class) + @ConvertGroup(to = BackupGroup.class) private FilterCriteria filterCriteria; @Schema( description = "How to handle external databases during backup", 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..980daeb5 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,12 +3,11 @@ 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; import jakarta.validation.groups.ConvertGroup; -import jakarta.validation.groups.Default; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; @@ -21,16 +20,15 @@ @Schema(description = "Response containing backup operation details") public class BackupResponse { - @NotBlank @Schema( - description = "Unique identifier of the backup", + description = "Unique name of the backup", examples = { "before-prod-update-20251013T1345-G5s8" }, required = true ) - private String backupName; @NotBlank + private String backupName; @Schema( description = "Name of the storage backend containing the backup", examples = { @@ -38,8 +36,8 @@ public class BackupResponse { }, required = true ) - private String storageName; @NotBlank + private String storageName; @Schema( description = "Path to the backup file in the storage", examples = { @@ -47,6 +45,7 @@ public class BackupResponse { }, required = true ) + @NotBlank private String blobPath; @Schema( description = "How to handle external databases during backup", @@ -57,20 +56,20 @@ public class BackupResponse { ) @NotNull private ExternalDatabaseStrategy externalDatabaseStrategy; - @NotNull @Schema( - description = "Whether external databases were skipped during the backup", + description = "Whether non‑backupable databases were ignored during backup", examples = { "false" } ) + @NotNull private boolean ignoreNotBackupableDatabases; @Schema( description = "Filter criteria", implementation = FilterCriteria.class ) @Valid - @ConvertGroup(from = Default.class, to = BackupGroup.class) + @ConvertGroup(to = BackupGroup.class) private FilterCriteria filterCriteria; @Schema( @@ -84,6 +83,7 @@ public class BackupResponse { "5" } ) + @NotNull private Integer total; @Schema( description = "Number of databases successfully backed up", @@ -91,6 +91,7 @@ public class BackupResponse { "3" } ) + @NotNull private Integer completed; @Schema( description = "Total size of the backup in bytes", @@ -98,6 +99,7 @@ public class BackupResponse { "1073741824" } ) + @NotNull private Long size; @Schema( description = "Error details if the backup failed", diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierDetailsResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierDetailsResponse.java new file mode 100644 index 00000000..9c801efc --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/ClassifierDetailsResponse.java @@ -0,0 +1,45 @@ +package com.netcracker.cloud.dbaas.dto.backupV2; + +import com.netcracker.cloud.dbaas.enums.ClassifierType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.SortedMap; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Classifier details used during restore operation") +public class ClassifierDetailsResponse { + @Schema( + description = "Type of classifier in restore context", + required = true, + implementation = ClassifierType.class + ) + private ClassifierType type; + + @Schema( + description = "Name of the existing database previously associated with this classifier," + + " used when the classifier replaces or transiently replaces another database during restore", + nullable = true, + examples = {"dbaas_12345"} + ) + private String previousDatabase; + + @Schema( + description = "Final classifier used to create a database in the target environment.", + examples = "{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}", + required = true + ) + private SortedMap classifier; + + @Schema( + description = "Original (pre-mapping) classifier from backup database preserved " + + "to track how mapping changed the classifier during restore", + examples = "{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}", + nullable = true + ) + 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..58c945d8 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,35 +1,34 @@ 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 @NoArgsConstructor @Schema(description = "Group of filters for backup and restore operations. Filters are applied in the following order:\n" + "\n" + - "1. `filter`: Apply the filter to the databases.\n" + - "2. `include`: Include databases that match any of the filters in the list.\n" + - "3. `exclude`: Exclude databases that match any of the filters in the list.") + "1. `include`\n" + + "2. `exclude`") public class FilterCriteria { - @Schema( - description = "Apply the filter to the remaining databases", - required = true - ) - @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; + @NotNull(groups = {BackupGroup.class}) + @Size(min = 1, groups = {BackupGroup.class}, message = "there should be at least one filter specified") + @Valid + private List include = new ArrayList<>(); + @Schema( description = "Exclude databases that match any of the filters in the list" ) - private List exclude; + @Valid + private List exclude = new ArrayList<>(); } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalBackupResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalBackupResponse.java index c733c679..5a58f07a 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalBackupResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalBackupResponse.java @@ -8,12 +8,18 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; @Data @AllArgsConstructor @Schema(description = "Logical backup details") public class LogicalBackupResponse { - + @Schema( + description = "Identifier of the logical backup", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema(description = "Name of the logical backup in adapter", required = true) String logicalBackupName; @Schema( diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalRestoreResponse.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalRestoreResponse.java index 5374c65b..be68b7b2 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalRestoreResponse.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/backupV2/LogicalRestoreResponse.java @@ -9,12 +9,19 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor @Schema(description = "Logical restore details") public class LogicalRestoreResponse { + @Schema( + description = "Identifier of the logical restore", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema(description = "Name of the logical restore in adapter", required = true) private String logicalRestoreName; @Schema( 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..6031b9f5 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 @@ -9,11 +9,18 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.UUID; @Data @NoArgsConstructor @Schema(description = "Logical database restore details") public class RestoreDatabaseResponse { + @Schema( + description = "Identifier of the restore database", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema( description = "Name of the database", examples = { @@ -26,7 +33,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\"}" @@ -34,7 +41,7 @@ public class RestoreDatabaseResponse { private List users; @Schema( description = "Database settings as a key-value map", - examples = "{\"key\":value, \"key\":value}" + examples = "{\"key\": \"value\", \"key\": \"value\"}" ) private Map settings; @Schema( @@ -58,7 +65,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 restore 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..dc7519b6 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 @@ -2,23 +2,31 @@ import lombok.Data; import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.List; -import java.util.SortedMap; +import java.util.UUID; @Data @NoArgsConstructor @Schema(description = "External database details") public class RestoreExternalDatabaseResponse { + @Schema( + description = "Identifier of the external restore database", + examples = {"550e8400-e29b-41d4-a716-446655440000"}, + required = true + ) + private UUID id; @Schema(description = "Name of the external database", examples = "mydb", required = true) private String name; @Schema(description = "Type of the database", examples = "postgresql") private String type; @Schema( - description = "List of database classifiers. Each classifier is a sorted map of attributes.", - examples = "[{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}]" + description = "List of classifier objects describing database attributes.", + implementation = ClassifierDetailsResponse.class, + type = SchemaType.ARRAY ) - 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..74f1aff9 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,11 +1,11 @@ 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; import lombok.Data; import lombok.NoArgsConstructor; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -15,12 +15,13 @@ @Schema(description = "Request to restore a database from a backup") public class RestoreRequest { @Schema( - description = "Unique identifier of the restore", + description = "Unique name of the restore", required = true, examples = { "restore-before-prod-update-20251203T1020-4t6S" } ) + @NotBlank private String restoreName; @Schema( description = "Name of the storage backend containing the restore", @@ -29,6 +30,7 @@ public class RestoreRequest { "s3-backend" } ) + @NotBlank private String storageName; @Schema( description = "Path to the restore file in the storage", @@ -37,13 +39,14 @@ public class RestoreRequest { "/backups" } ) + @NotBlank private String blobPath; @Schema( description = "Filter criteria", implementation = FilterCriteria.class ) @Valid - @ConvertGroup(from = Default.class, to = RestoreGroup.class) + @ConvertGroup(to = RestoreGroup.class) private FilterCriteria filterCriteria; @Schema( description = "Mapping to use for the restore operation", 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..991c49ab 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,11 +2,11 @@ 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.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.groups.ConvertGroup; -import jakarta.validation.groups.Default; import lombok.AllArgsConstructor; import lombok.Data; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; @@ -19,19 +19,21 @@ @Schema(description = "Response containing the restore operation details") public class RestoreResponse { @Schema( - description = "Unique identifier of the restore", + description = "Unique name of the restore", examples = { "restore-before-prod-update-20251203T1020-4t6S" }, required = true ) + @NotBlank private String restoreName; @Schema( - description = "Unique identifier of the backup", + description = "Unique name of the backup", examples = { "before-prod-update-20251013T1345-G5s8" } ) + @NotBlank private String backupName; @Schema( description = "Name of the storage backend containing the restore", @@ -39,6 +41,7 @@ public class RestoreResponse { "s3-backend" } ) + @NotBlank private String storageName; @Schema( description = "Path to the restore file in the storage", @@ -61,7 +64,7 @@ public class RestoreResponse { implementation = FilterCriteria.class ) @Valid - @ConvertGroup(from = Default.class, to = RestoreGroup.class) + @ConvertGroup(to = RestoreGroup.class) private FilterCriteria filterCriteria; @Schema( description = "Mapping configuration for the restore", @@ -78,21 +81,19 @@ public class RestoreResponse { description = "Total number of databases being restored", examples = "5" ) + @NotNull private Integer total; @Schema( description = "Completed databases restore operation", examples = "5" ) + @NotNull private Integer completed; @Schema( description = "Aggregated error messages during restore operation", 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/AdapterBackupKey.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/AdapterBackupKey.java new file mode 100644 index 00000000..3d598ea8 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/AdapterBackupKey.java @@ -0,0 +1,4 @@ +package com.netcracker.cloud.dbaas.entity.dto.backupV2; + +public record AdapterBackupKey(String adapterId, String logicalBackupName) { +} 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 deleted file mode 100644 index a30e6453..00000000 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/BackupDatabaseDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.netcracker.cloud.dbaas.entity.dto.backupV2; - -import com.netcracker.cloud.dbaas.entity.pg.backupV2.BackupDatabase; - -import java.util.List; -import java.util.SortedMap; - - -public record BackupDatabaseDelegate(BackupDatabase backupDatabase, - List> classifiers) {} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/DatabaseWithClassifiers.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/DatabaseWithClassifiers.java new file mode 100644 index 00000000..950acbfe --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/dto/backupV2/DatabaseWithClassifiers.java @@ -0,0 +1,10 @@ +package com.netcracker.cloud.dbaas.entity.dto.backupV2; + +import com.netcracker.cloud.dbaas.entity.pg.backupV2.ClassifierDetails; +import com.netcracker.cloud.dbaas.entity.pg.backupV2.BackupDatabase; + +import java.util.List; + + +public record DatabaseWithClassifiers(BackupDatabase backupDatabase, + List classifiers) {} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Backup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Backup.java index ca2e0f48..f436454d 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Backup.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Backup.java @@ -5,7 +5,10 @@ import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -13,7 +16,6 @@ @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupDatabase.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupDatabase.java index c56282c7..e55dda75 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupDatabase.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupDatabase.java @@ -5,7 +5,6 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; @@ -18,7 +17,6 @@ import java.util.UUID; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -26,7 +24,6 @@ public class BackupDatabase { @Id - @GeneratedValue private UUID id; @ManyToOne @@ -69,11 +66,20 @@ public class BackupDatabase { private Instant creationTime; @Data - @Builder @NoArgsConstructor @AllArgsConstructor public static class User { String name; String role; } + + public BackupDatabase(UUID id, LogicalBackup logicalBackup, String name, List> classifiers, Map settings, List users, boolean configurational) { + this.id = id; + this.logicalBackup = logicalBackup; + this.name = name; + this.classifiers = classifiers; + this.settings = settings; + this.users = users; + this.configurational = configurational; + } } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupExternalDatabase.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupExternalDatabase.java index 7b1fff0b..d0a952b0 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupExternalDatabase.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/BackupExternalDatabase.java @@ -4,7 +4,6 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; @@ -15,7 +14,6 @@ import java.util.UUID; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -23,7 +21,6 @@ public class BackupExternalDatabase { @Id - @GeneratedValue private UUID id; @ManyToOne diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/ClassifierDetails.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/ClassifierDetails.java new file mode 100644 index 00000000..02e16d17 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/ClassifierDetails.java @@ -0,0 +1,30 @@ +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 ClassifierDetails { + private ClassifierType type; + private String previousDatabase; + private SortedMap classifier; + private SortedMap classifierBeforeMapper; + + @Override + public boolean equals(Object o) { + if (!(o instanceof ClassifierDetails 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/FilterCriteriaEntity.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterCriteriaEntity.java index 1988ffc7..74b2e43c 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterCriteriaEntity.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterCriteriaEntity.java @@ -1,14 +1,11 @@ package com.netcracker.cloud.dbaas.entity.pg.backupV2; -import lombok.Builder; import lombok.Data; import java.util.List; @Data -@Builder public class FilterCriteriaEntity { - private List filter; private List include; private List exclude; } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterEntity.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterEntity.java index 66a256c4..e4d8ccde 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterEntity.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/FilterEntity.java @@ -2,13 +2,11 @@ import com.netcracker.cloud.dbaas.dto.backupV2.DatabaseKind; import com.netcracker.cloud.dbaas.dto.backupV2.DatabaseType; -import lombok.Builder; import lombok.Data; import java.util.List; @Data -@Builder public class FilterEntity { private List namespace; private List microserviceName; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalBackup.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalBackup.java index 16380155..ceddd62d 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalBackup.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalBackup.java @@ -4,16 +4,18 @@ import com.fasterxml.jackson.annotation.JsonManagedReference; import com.netcracker.cloud.dbaas.enums.BackupTaskStatus; import jakarta.persistence.*; -import lombok.*; -import org.eclipse.microprofile.openapi.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.UUID; @Data -@Builder @NoArgsConstructor @AllArgsConstructor @Entity @@ -21,8 +23,6 @@ public class LogicalBackup { @Id - @GeneratedValue - @Schema(description = "A unique identifier of the logical backup process.", required = true) private UUID id; @Column(name = "logical_backup_name") @@ -41,7 +41,7 @@ public class LogicalBackup { @ToString.Exclude @JsonManagedReference @OneToMany(mappedBy = "logicalBackup", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private List backupDatabases; + private List backupDatabases = new ArrayList<>(); @Enumerated(EnumType.STRING) @Column(name = "status") @@ -56,6 +56,13 @@ public class LogicalBackup { @Column(name = "completion_time") private Instant completionTime; + public LogicalBackup(UUID id, Backup backup, String adapterId, String type) { + this.id = id; + this.backup = backup; + this.adapterId = adapterId; + this.type = type; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalRestore.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalRestore.java index 7aeab9f4..7af1d8a8 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalRestore.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/LogicalRestore.java @@ -2,7 +2,10 @@ import com.netcracker.cloud.dbaas.enums.RestoreTaskStatus; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; import java.time.Instant; import java.util.List; @@ -10,7 +13,6 @@ import java.util.UUID; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -18,7 +20,6 @@ public class LogicalRestore { @Id - @GeneratedValue private UUID id; @Column(name = "logical_restore_name") @@ -39,7 +40,7 @@ public class LogicalRestore { @Enumerated(EnumType.STRING) @Column(name = "status") - private RestoreTaskStatus status; + private RestoreTaskStatus status = RestoreTaskStatus.NOT_STARTED; @Column(name = "error_message") private String errorMessage; diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Restore.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Restore.java index f86ecdbc..7985eb47 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Restore.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/backupV2/Restore.java @@ -5,7 +5,10 @@ import com.netcracker.cloud.dbaas.enums.RestoreStatus; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -14,7 +17,6 @@ import java.util.Objects; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -65,8 +67,6 @@ public class Restore { private Integer completed; - private Long duration; - @Column(name = "error_message") private String errorMessage; @@ -74,7 +74,6 @@ public class Restore { private int attemptCount = 0; @Data - @Builder @NoArgsConstructor @AllArgsConstructor public static class MappingEntity { @@ -86,6 +85,10 @@ public void incrementAttempt() { this.attemptCount++; } + public void resetAttempt() { + this.attemptCount = 0; + } + @Override public boolean equals(Object o) { if (!(o instanceof Restore restore)) return false; 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..1e8856c6 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 @@ -4,7 +4,6 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; @@ -13,11 +12,9 @@ import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.SortedMap; import java.util.UUID; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -25,7 +22,6 @@ public class RestoreDatabase { @Id - @GeneratedValue private UUID id; @ManyToOne @@ -41,7 +37,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..a9bd9e22 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 @@ -4,18 +4,15 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; import java.util.List; -import java.util.SortedMap; import java.util.UUID; @Data -@Builder @AllArgsConstructor @NoArgsConstructor(force = true) @Entity @@ -23,7 +20,6 @@ public class RestoreExternalDatabase { @Id - @GeneratedValue private UUID id; @ManyToOne @@ -40,5 +36,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/BackupTaskStatus.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/BackupTaskStatus.java index f0e51c99..0f0279cc 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/BackupTaskStatus.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/BackupTaskStatus.java @@ -1,5 +1,5 @@ package com.netcracker.cloud.dbaas.enums; public enum BackupTaskStatus { - NOT_STARTED, IN_PROGRESS, FAILED, COMPLETED + NOT_STARTED, IN_PROGRESS, FAILED, RETRYABLE_FAIL, COMPLETED } 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/enums/RestoreTaskStatus.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/RestoreTaskStatus.java index 570976d2..131459db 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/RestoreTaskStatus.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/enums/RestoreTaskStatus.java @@ -1,5 +1,5 @@ package com.netcracker.cloud.dbaas.enums; public enum RestoreTaskStatus { - NOT_STARTED, IN_PROGRESS, FAILED, COMPLETED + NOT_STARTED, IN_PROGRESS, FAILED, RETRYABLE_FAIL, COMPLETED } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/ErrorCodes.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/ErrorCodes.java index ef079558..2a536a27 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/ErrorCodes.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/ErrorCodes.java @@ -214,8 +214,8 @@ public enum ErrorCodes implements ErrorCode { "Resource with name '%s' already exists"), CORE_DBAAS_4047( "CORE-DBAAS-4047", - "Backup not allowed", - "The backup/restore request can`t be processed. %s" + "Operation not allowed", + "The backup/restore request can't be processed. %s" ), CORE_DBAAS_4048( "CORE-DBAAS-4048", @@ -225,7 +225,7 @@ public enum ErrorCodes implements ErrorCode { CORE_DBAAS_4049( "CORE-DBAAS-4049", "Unprocessable resource", - "Resource '%s' can`t be processed: %s" + "Resource '%s' can't be processed: %s" ), CORE_DBAAS_4050( "CORE-DBAAS-4050", @@ -242,6 +242,11 @@ public enum ErrorCodes implements ErrorCode { "Digest mismatch", "Digest header mismatch: %s" ), + CORE_DBAAS_4053( + "CORE-DBAAS-4053", + "Operation already running", + "Operation '%s' is already in progress" + ), CORE_DBAAS_7002( "CORE-DBAAS-7002", "trackingId not found", diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/OperationAlreadyRunningException.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/OperationAlreadyRunningException.java new file mode 100644 index 00000000..d22fb78c --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/exceptions/OperationAlreadyRunningException.java @@ -0,0 +1,9 @@ +package com.netcracker.cloud.dbaas.exceptions; + +import com.netcracker.cloud.core.error.runtime.ErrorCodeException; + +public class OperationAlreadyRunningException extends ErrorCodeException { + public OperationAlreadyRunningException(String operation) { + super(ErrorCodes.CORE_DBAAS_4053, ErrorCodes.CORE_DBAAS_4053.getDetail(operation)); + } +} 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..3e169f84 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 @@ -78,7 +78,7 @@ default RestoreTaskStatus toRestoreTaskStatus(String status) { return mapStatus(status, RestoreTaskStatus::valueOf); } - private static , R extends Enum> R mapStatus( + private static > R mapStatus( String status, Function resultStatusGetter) { if (status == null) { @@ -88,8 +88,7 @@ private static , R extends Enum> R mapStatus( } return switch (status) { - case "notStarted" -> resultStatusGetter.apply("NOT_STARTED"); - case "inProgress" -> resultStatusGetter.apply("IN_PROGRESS"); + case "notStarted", "inProgress" -> resultStatusGetter.apply("IN_PROGRESS"); case "completed" -> resultStatusGetter.apply("COMPLETED"); case "failed" -> resultStatusGetter.apply("FAILED"); default -> throw new UnprocessableEntityException( @@ -99,8 +98,13 @@ private static , R extends Enum> R mapStatus( }; } - @Mapping(target = "id", ignore = true) - RestoreExternalDatabase toRestoreExternalDatabase(BackupExternalDatabase backupExternalDatabase); + @Mapping(target = "id", expression = "java(java.util.UUID.randomUUID())") + @Mapping(target = "name", source = "backupExternalDatabase.name") + @Mapping(target = "type", source = "backupExternalDatabase.type") + @Mapping(target = "classifiers", source = "classifiers") + RestoreExternalDatabase toRestoreExternalDatabase(BackupExternalDatabase backupExternalDatabase, List classifiers); - List toRestoreExternalDatabases(List backupExternalDatabases); + ClassifierDetailsResponse toClassifierResponse(ClassifierDetails 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/BackupRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/BackupRepository.java index 8abb52fc..61b6831d 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/BackupRepository.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/BackupRepository.java @@ -15,11 +15,10 @@ public class BackupRepository implements PanacheRepositoryBase { public Backup save(Backup backup) { EntityManager entityManager = getEntityManager(); - entityManager.merge(backup); - return backup; + return entityManager.merge(backup); } - public List findBackupsToAggregate() { - return list("status in ?1", List.of(BackupStatus.NOT_STARTED, BackupStatus.IN_PROGRESS)); + public List findBackupsToTrack() { + return list("status in ?1", List.of(BackupStatus.IN_PROGRESS)); } } 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..264eacd5 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,60 @@ 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 " + ); + + int index = 0; + boolean needDatabaseJoin = false; + 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) { + needDatabaseJoin = true; + 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 = '') "); + } + } + if (!query.isEmpty()) { + String block = "(" + String.join(" AND ", query) + ")"; + orBlock.add(block); + index++; + } + } + + if (needDatabaseJoin) + q.append("LEFT JOIN database d ON cl.database_id = d.id "); + + if (!orBlock.isEmpty()) + q.append("WHERE ").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/repositories/pg/jpa/RestoreRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/RestoreRepository.java index 34034971..87fada69 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/RestoreRepository.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/RestoreRepository.java @@ -15,12 +15,11 @@ public class RestoreRepository implements PanacheRepositoryBase public Restore save(Restore restore) { EntityManager entityManager = getEntityManager(); - entityManager.merge(restore); - return restore; + return entityManager.merge(restore); } - public List findRestoresToAggregate() { - return list("status in ?1", List.of(RestoreStatus.NOT_STARTED, RestoreStatus.IN_PROGRESS)); + public List findRestoresToTrack() { + return list("status in ?1", List.of(RestoreStatus.IN_PROGRESS)); } public long countNotCompletedRestores() { 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..2863b314 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,10 +38,13 @@ 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 java.util.stream.Stream; import static com.netcracker.cloud.dbaas.Constants.*; import static com.netcracker.cloud.dbaas.entity.shared.AbstractDbState.DatabaseStateStatus.CREATED; +import static com.netcracker.cloud.dbaas.enums.RestoreTaskStatus.NOT_STARTED; import static com.netcracker.cloud.dbaas.service.DBaaService.MARKED_FOR_DROP; import static io.quarkus.scheduler.Scheduled.ConcurrentExecution.SKIP; @@ -56,6 +59,12 @@ public class DbBackupV2Service { private static final String TRACK_RESTORE_OPERATION = "trackRestoreV2"; private static final String RESTORE = "restore"; private static final String DATABASE_NAME = "databaseName"; + private static final String LOGICAL_BACKUP = "logicalBackup"; + private static final String LOGICAL_RESTORE = "logicalRestore"; + private static final String RESTORE_DATABASE = "restoreDatabase"; + + private static final Integer LOCK_AT_MOST = 2; + private static final Integer LOCK_AT_LEAST = 0; private final BackupRepository backupRepository; private final RestoreRepository restoreRepository; @@ -106,10 +115,9 @@ public DbBackupV2Service(BackupRepository backupRepository, } public BackupResponse backup(BackupRequest backupRequest, boolean dryRun) { - String backupName = backupRequest.getBackupName(); - backupExistenceCheck(backupName); + backupExistenceCheck(backupRequest.getBackupName()); - log.info("Start backup process with name {}", backupName); + log.info("Start backup process with backupRequest {}", backupRequest); Map> filteredDb = validateAndFilterDatabasesForBackup( getAllDbByFilter(backupRequest.getFilterCriteria()), backupRequest.getIgnoreNotBackupableDatabases(), @@ -119,13 +127,13 @@ public BackupResponse backup(BackupRequest backupRequest, boolean dryRun) { Backup backup = initializeFullBackupStructure(filteredDb, backupRequest); if (!dryRun) { // Saving initialized backup structure - backupRepository.save(backup); + backup = backupRepository.save(backup); startBackup(backup); } updateAggregatedStatus(backup); if (!dryRun) { // Saving aggregated backup structure - backupRepository.save(backup); + backup = backupRepository.save(backup); } return mapper.toBackupResponse(backup); } @@ -169,14 +177,15 @@ protected Backup initializeFullBackupStructure(Map { Database database = entry.getKey(); List databaseRegistries = entry.getValue(); - return BackupExternalDatabase.builder() - .backup(backup) - .name(database.getName()) - .type(database.getDatabaseRegistry().getFirst().getType()) - .classifiers(databaseRegistries.stream() + return new BackupExternalDatabase( + UUID.randomUUID(), + backup, + database.getName(), + database.getDatabaseRegistry().getFirst().getType(), + databaseRegistries.stream() .map(DatabaseRegistry::getClassifier) - .toList()) - .build(); + .toList() + ); }).toList(); // Persist and return @@ -188,33 +197,29 @@ protected Backup initializeFullBackupStructure(Map> databaseToRegistry, Backup backup) { DbaasAdapter adapter = physicalDatabasesService.getAdapterById(adapterId); - if (!isBackupRestoreSupported(adapter)) { - log.error("Adapter {} not support backup operation", adapterId); + if (isBackupRestoreUnsupported(adapter)) { + log.error("Adapter {} does not support backup operation", adapterId); throw new DatabaseBackupRestoreNotSupportedException( - String.format("Adapter %s not support backup operation", adapterId), + String.format("Adapter %s does not support backup operation", adapterId), Source.builder().build()); } - LogicalBackup logicalBackup = LogicalBackup.builder() - .backup(backup) - .adapterId(adapterId) - .type(adapter.type()) - .backupDatabases(new ArrayList<>()) - .build(); + LogicalBackup logicalBackup = new LogicalBackup(UUID.randomUUID(), backup, adapterId, adapter.type()); + // Initializing backup database entity logicalBackup.getBackupDatabases().addAll(databaseToRegistry.entrySet().stream() .map(entry -> { Database db = entry.getKey(); List databaseRegistries = entry.getValue(); - return BackupDatabase.builder() - .logicalBackup(logicalBackup) - .name(DbaasBackupUtils.getDatabaseName(db)) - .classifiers(databaseRegistries.stream() - .map(DatabaseRegistry::getClassifier).toList()) - .users(getBackupDatabaseUsers(db.getConnectionProperties())) - .settings(db.getSettings()) - .configurational(db.getBgVersion() != null && !db.getBgVersion().isBlank()) - .build(); + return new BackupDatabase( + UUID.randomUUID(), + logicalBackup, + DbaasBackupUtils.getDatabaseName(db), + databaseRegistries.stream().map(DatabaseRegistry::getClassifier).toList(), + db.getSettings(), + getBackupDatabaseUsers(db.getConnectionProperties()), + db.getBgVersion() != null && !db.getBgVersion().isBlank() + ); }).toList()); return logicalBackup; } @@ -238,14 +243,13 @@ protected void startBackup(Backup backup) { refreshLogicalBackupState(logicalBackup, response) ) .exceptionally(throwable -> { - logicalBackup.setStatus(BackupTaskStatus.FAILED); + logicalBackup.setStatus(BackupTaskStatus.RETRYABLE_FAIL); logicalBackup.setErrorMessage(extractErrorMessage(throwable)); return null; })) .toList(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - backupRepository.save(backup); } protected LogicalBackupAdapterResponse startLogicalBackup(LogicalBackup logicalBackup) { @@ -258,7 +262,7 @@ protected LogicalBackupAdapterResponse startLogicalBackup(LogicalBackup logicalB .map(db -> Map.of(DATABASE_NAME, db.getName())) .toList(); - RetryPolicy retryPolicy = buildRetryPolicy(logicalBackup.getLogicalBackupName(), BACKUP_OPERATION); + RetryPolicy retryPolicy = buildRetryPolicy(BACKUP_OPERATION, LOGICAL_BACKUP, logicalBackup.getId().toString(), adapterId); try { return Failsafe.with(retryPolicy).get(() -> { @@ -307,7 +311,7 @@ public void checkBackupsAsync() { //TODO propagate correct business id log.info("Starting backup scheduler"); LockAssert.assertLocked(); - List backupsToAggregate = backupRepository.findBackupsToAggregate(); + List backupsToAggregate = backupRepository.findBackupsToTrack(); backupsToAggregate.forEach(this::trackAndAggregate); } @@ -326,9 +330,11 @@ protected void trackAndAggregate(Backup backup) { private void fetchAndUpdateStatuses(Backup backup) { List notFinishedBackups = backup.getLogicalBackups().stream() - .filter(db -> db.getStatus() == BackupTaskStatus.IN_PROGRESS - || db.getStatus() == BackupTaskStatus.NOT_STARTED) - .toList(); + .filter(db -> + db.getStatus() == BackupTaskStatus.IN_PROGRESS + || db.getStatus() == BackupTaskStatus.NOT_STARTED + || db.getStatus() == BackupTaskStatus.RETRYABLE_FAIL + ).toList(); List> futures = notFinishedBackups.stream() .map(this::trackLogicalBackupAsync) @@ -339,7 +345,7 @@ private void fetchAndUpdateStatuses(Backup backup) { private CompletableFuture trackLogicalBackupAsync(LogicalBackup logicalBackup) { RetryPolicy retryPolicy = - buildRetryPolicy(logicalBackup.getLogicalBackupName(), TRACK_BACKUP_OPERATION); + buildRetryPolicy(TRACK_BACKUP_OPERATION, LOGICAL_BACKUP, logicalBackup.getId().toString(), logicalBackup.getAdapterId()); return CompletableFuture.supplyAsync( asyncOperations.wrapWithContext(() -> Failsafe.with(retryPolicy) @@ -348,6 +354,7 @@ private CompletableFuture trackLogicalBackupAsync(LogicalBackup logicalBac ) .thenAccept(response -> refreshLogicalBackupState(logicalBackup, response)) .exceptionally(throwable -> { + logicalBackup.setStatus(BackupTaskStatus.RETRYABLE_FAIL); logicalBackup.setErrorMessage(extractErrorMessage(throwable)); return null; }); @@ -355,6 +362,12 @@ private CompletableFuture trackLogicalBackupAsync(LogicalBackup logicalBac private LogicalBackupAdapterResponse executeTrackBackup(LogicalBackup logicalBackup) { DbaasAdapter adapter = physicalDatabasesService.getAdapterById(logicalBackup.getAdapterId()); + + if (isLogicalBackupNotStarted(logicalBackup)) { + LogicalBackupAdapterResponse response = startLogicalBackup(logicalBackup); + refreshLogicalBackupState(logicalBackup, response); + } + LogicalBackupAdapterResponse response = adapter.trackBackupV2( logicalBackup.getLogicalBackupName(), logicalBackup.getBackup().getStorageName(), @@ -362,9 +375,9 @@ private LogicalBackupAdapterResponse executeTrackBackup(LogicalBackup logicalBac ); if (response == null) { - log.error("Empty response from {} for {}", TRACK_BACKUP_OPERATION, logicalBackup.getLogicalBackupName()); + log.error("Operation {} return empty response from adapter={} for logicalBackup={}", TRACK_BACKUP_OPERATION, logicalBackup.getAdapterId(), logicalBackup.getId()); //not obvious from throw new BackupExecutionException( - String.format("Empty response from %s for %s", TRACK_BACKUP_OPERATION, logicalBackup.getLogicalBackupName()), + String.format("Operation %s return empty response from adapter=%s for logicalBackup=%s", TRACK_BACKUP_OPERATION, logicalBackup.getAdapterId(), logicalBackup.getId()), new Throwable() ); } @@ -403,62 +416,42 @@ protected void updateAggregatedStatus(Backup backup) { } } - backup.setStatus(aggregateBackupStatus(statusSet)); + backup.setStatus(aggregateBackupTaskStatus(statusSet)); backup.setSize(totalBytes); backup.setTotal(totalDbCount); backup.setCompleted(completedDbCount); backup.setErrorMessage(String.join("; ", errorMessages)); } - protected BackupStatus aggregateBackupStatus(Set statusSet) { - return aggregateStatus(statusSet, - BackupTaskStatus::valueOf, - BackupStatus::valueOf); - } - - protected RestoreStatus aggregateRestoreStatus(Set statusSet) { - return aggregateStatus(statusSet, - RestoreTaskStatus::valueOf, - RestoreStatus::valueOf); - } - public BackupStatusResponse getCurrentStatus(String backupName) { return mapper.toBackupStatusResponse(getBackupOrThrowException(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"); - } - - String namespace = filter.getNamespace().getFirst(); - - List databasesRegistriesForBackup = databaseRegistryDbaasRepository - .findAnyLogDbRegistryTypeByNamespace(namespace) + List filteredDatabases = databaseRegistryDbaasRepository + .findAllDatabasesByFilter(filterCriteria.getInclude()) .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 +461,43 @@ 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 isFilled(Filter f) { + return !f.getNamespace().isEmpty() + || !f.getMicroserviceName().isEmpty() + || !f.getDatabaseType().isEmpty() + || !f.getDatabaseKind().isEmpty(); + } + + 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)); } @@ -477,7 +507,7 @@ public BackupResponse getBackupMetadata(String backupName) { if (BackupStatus.COMPLETED != backup.getStatus()) { throw new UnprocessableEntityException(backupName, - String.format("can`t produce metadata for backup in status %s", backup.getStatus()), + String.format("can't produce metadata for backup %s in status %s", backupName, backup.getStatus()), Source.builder().build()); } return mapper.toBackupResponse(backup); @@ -514,7 +544,7 @@ public void uploadBackupMetadata(BackupResponse backupResponse) { } } else { throw new IllegalResourceStateException( - String.format("can`t restore %s backup that not imported", BackupStatus.DELETED), + String.format("can't restore a %s backup that is not imported", BackupStatus.DELETED), Source.builder().build() ); } @@ -522,7 +552,7 @@ public void uploadBackupMetadata(BackupResponse backupResponse) { // if backup status not DELETED throw new IllegalResourceStateException( - String.format("backup already exists and is not %s status", BackupStatus.DELETED), + String.format("backup already exists and is not in %s status", BackupStatus.DELETED), Source.builder().build() ); } @@ -575,7 +605,7 @@ private CompletableFuture deleteLogicalBackupAsync( ) { String adapterId = logicalBackup.getAdapterId(); RetryPolicy retryPolicy = - buildRetryPolicy(logicalBackup.getLogicalBackupName(), DELETE_BACKUP_OPERATION); + buildRetryPolicy(DELETE_BACKUP_OPERATION, LOGICAL_BACKUP, logicalBackup.getId().toString(), adapterId); log.info("Backup with adapterId {} has {} databases to delete", adapterId, logicalBackup.getBackupDatabases().size()); @@ -614,155 +644,131 @@ private void finalizeBackupDeletion(Backup backup, Map failedAda backupRepository.save(backup); } - public RestoreResponse restore(String backupName, RestoreRequest restoreRequest, boolean dryRun) { - if (dryRun) - throw new FunctionalityNotImplemented("dryRun"); - + public RestoreResponse restore(String backupName, RestoreRequest restoreRequest, boolean dryRun, boolean allowParallel) { String restoreName = restoreRequest.getRestoreName(); if (restoreRepository.findByIdOptional(restoreName).isPresent()) { log.error("Restore with name {} already exists", restoreName); throw new ResourceAlreadyExistsException(restoreName, Source.builder().build()); } - log.info("Start restore for backup {}", backupName); + log.info("Start restore {} for backup {}", restoreName, backupName); Backup backup = getBackupOrThrowException(backupName); + checkBackupStatusForRestore(restoreName, 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()); + if (dryRun) { + return applyDryRunRestore(backup, restoreRequest); } - 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; - - // DryRun on adapters - startRestore(restore, true); + Restore restore = restoreLockWrapper(() -> { + Restore currRestore = initializeFullRestoreStructure(backup, restoreRequest); + return restoreRepository.save(currRestore); + }, allowParallel); + + // DryRun on adapters + startRestore(restore, true); + aggregateRestoreStatus(restore); + if (RestoreStatus.FAILED != restore.getStatus()) { + // Real run on adapters + startRestore(restore, false); aggregateRestoreStatus(restore); - if (RestoreStatus.FAILED != restore.getStatus()) { - // Real run on adapters - restore = getRestoreOrThrowException(restoreName); - startRestore(restore, false); - aggregateRestoreStatus(restore); - } - restoreRepository.save(restore); - return mapper.toRestoreResponse(restore); - } finally { - if (!unlocked) { - lock.unlock(); - } } + + restoreRepository.save(restore); + return mapper.toRestoreResponse(restore); } - protected List getAllDbByFilter(List backupDatabasesToFilter, FilterCriteria filterCriteria) { + private RestoreResponse applyDryRunRestore(Backup backup, RestoreRequest restoreRequest) { + Restore currRestore = initializeFullRestoreStructure(backup, restoreRequest); + startRestore(currRestore, true); + aggregateRestoreStatus(currRestore); + return mapper.toRestoreResponse(currRestore); + } + + 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 DatabaseWithClassifiers( + db, + db.getClassifiers().stream() + .map(c -> new ClassifierDetails(ClassifierType.NEW, null, null, new TreeMap<>(c))) + .toList() + ) + ) .toList(); - Filter filter = filterCriteria.getFilter().getFirst(); + return backupDatabasesToFilter.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.getLogicalBackup().getType(); + boolean configurational = db.isConfigurational(); + return filterCriteria.getInclude().stream().anyMatch(filter -> isMatches(filter, namespace, microserviceName, type, configurational)) + && filterCriteria.getExclude().stream().noneMatch(ex -> isMatches(ex, namespace, microserviceName, type, configurational)); + }) + .map(c -> new ClassifierDetails(ClassifierType.NEW, null, null, new TreeMap<>(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(); - // 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(); - - if (filteredClassifiers.isEmpty()) - return null; + if (filteredClassifiers.isEmpty()) { + return null; + } - return new BackupDatabaseDelegate( - backupDatabase, - filteredClassifiers - ); - } - ) + return new DatabaseWithClassifiers(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()); - } - return databaseDelegateList; } protected Restore initializeFullRestoreStructure( Backup backup, RestoreRequest restoreRequest ) { - // Apply ExternalDatabaseStrategy to external databases, filter by FilterCriteria - List externalDatabases = validateAndFilterExternalDb( + // Apply ExternalDatabaseStrategy to external databases and filter by FilterCriteria + List filteredExternalDbs = validateAndFilterExternalDb( backup.getExternalDatabases(), restoreRequest.getExternalDatabaseStrategy(), restoreRequest.getFilterCriteria()); - // MappingEntity classifiers of externalDb - if (restoreRequest.getMapping() != null) - externalDatabases = executeMappingForExternalDb(externalDatabases, restoreRequest.getMapping()); - - // Filtering classifiers - List backupDatabases = getAllDbByFilter( + // Filter internal database classifiers + List filteredBackupDatabases = getAllDbByFilter( backup.getLogicalBackups().stream() .flatMap(logicalBackup -> logicalBackup.getBackupDatabases().stream()) .toList(), restoreRequest.getFilterCriteria()); + if (filteredExternalDbs.isEmpty() && filteredBackupDatabases.isEmpty()) { + log.warn("Databases that match filterCriteria during restore not found"); + throw new DbNotFoundException("Databases that match filterCriteria not found", Source.builder().build()); + } + + // Mapping classifiers + List mappedExternalDbs = + executeMappingForExternalDb(filteredExternalDbs, restoreRequest.getMapping()); + List mappedBackupDatabases = + applyMappingToBackupDatabases(filteredBackupDatabases, restoreRequest.getMapping()); + + checkForCollision(mappedExternalDbs, mappedBackupDatabases); + // Enrich classifiers + List enrichedBackupClassifiers = enrichInternalDbClassifiers(mappedBackupDatabases); + List enrichedExternalDbs = enrichExternalDbClassifiers(mappedExternalDbs); - // Group BackupDatabase by updated adapters - Map> groupedByTypeAndAdapter = - groupBackupDatabasesByTypeAndAdapter(backupDatabases, restoreRequest.getMapping()); + // Group BackupDatabases by updated adapter and logicalBackupName + Map> groupedByTypeAndAdapter = + groupBackupDatabasesByLogicalBackupNameAndAdapter(enrichedBackupClassifiers); log.info("Initializing restore structure: restoreName={}, backupName={}", restoreRequest.getRestoreName(), backup.getName()); + // Build logicalRestores for each new adapter List logicalRestores = groupedByTypeAndAdapter.entrySet().stream() .map(entry -> { LogicalRestore logicalRestore = new LogicalRestore(); - logicalRestore.setType(entry.getKey().getType()); - logicalRestore.setAdapterId(entry.getKey().getAdapter().getAdapterId()); + logicalRestore.setId(UUID.randomUUID()); + logicalRestore.setType(entry.getValue().getFirst().backupDatabase().getLogicalBackup().getType()); + logicalRestore.setAdapterId(entry.getKey().adapterId()); List restoreDatabases = createRestoreDatabases(entry.getValue()); @@ -782,99 +788,190 @@ protected Restore initializeFullRestoreStructure( restore.setBlobPath(restoreRequest.getBlobPath()); restore.setLogicalRestores(new ArrayList<>(logicalRestores)); restore.setExternalDatabaseStrategy(restoreRequest.getExternalDatabaseStrategy()); - restore.setExternalDatabases(externalDatabases); + restore.setExternalDatabases(enrichedExternalDbs); restore.setMapping(mapper.toMappingEntity(restoreRequest.getMapping())); restore.setFilterCriteria(mapper.toFilterCriteriaEntity(restoreRequest.getFilterCriteria())); // set up relation logicalRestores.forEach(lr -> lr.setRestore(restore)); - externalDatabases.forEach(db -> db.setRestore(restore)); + enrichedExternalDbs.forEach(db -> db.setRestore(restore)); int totalDatabases = logicalRestores.stream() .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, enrichedExternalDbs.size()); return restore; } - private List validateAndFilterExternalDb(List externalDatabases, - ExternalDatabaseStrategy strategy, - FilterCriteria filterCriteria) { - if (externalDatabases == null || externalDatabases.isEmpty()) + private List applyMappingToBackupDatabases(List backupDatabases, Mapping mapping) { + return backupDatabases.stream() + .map(db -> + new DatabaseWithClassifiers(db.backupDatabase(), applyMapping(db.classifiers(), mapping)) + ).toList(); + } + + protected List applyMapping(List classifiers, Mapping mapping) { + for (ClassifierDetails classifier : classifiers) { + SortedMap sourceClassifier = classifier.getClassifierBeforeMapper(); + SortedMap mappedClassifier = new TreeMap<>(sourceClassifier); + if (mapping != null) { + String targetNamespace = applyMapping(mapping.getNamespaces(), (String) sourceClassifier.get(NAMESPACE)); + String targetTenant = (String) sourceClassifier.get(TENANT_ID); + mappedClassifier.put(NAMESPACE, targetNamespace); + + if (targetTenant != null) { + targetTenant = applyMapping(mapping.getTenants(), targetTenant); + mappedClassifier.put(TENANT_ID, targetTenant); + } + } + classifier.setClassifier(mappedClassifier); + } + return classifiers; + } + + private List enrichInternalDbClassifiers(List backupDatabases) { + return backupDatabases.stream() + .map(db -> { + String type = db.backupDatabase().getLogicalBackup().getType(); + List updatedClassifiers = findSimilarDbByClassifier(db.classifiers(), type).stream().toList(); + return new DatabaseWithClassifiers(db.backupDatabase(), updatedClassifiers); + }).toList(); + } + + private List enrichExternalDbClassifiers(List externalDatabases) { + List enrichedExternalDbs = new ArrayList<>(); + + for (RestoreExternalDatabase db : externalDatabases) { + List classifiers = findSimilarDbByClassifier(db.getClassifiers(), db.getType()).stream().toList(); + db.setClassifiers(classifiers); + enrichedExternalDbs.add(db); + } + + return enrichedExternalDbs; + } + + private void checkForCollision(List externalDatabases, List backupDatabases) { + Set> uniqueClassifiers = new HashSet<>(); + List duplicateClassifiers = new ArrayList<>(); + + Stream.concat( + externalDatabases.stream().flatMap(ext -> ext.getClassifiers().stream()), + backupDatabases.stream().flatMap(db -> db.classifiers().stream()) + ).forEach(classifier -> { + SortedMap c = classifier.getClassifier(); + if (!uniqueClassifiers.add(c)) { + duplicateClassifiers.add(classifier); + } + }); + + if (!duplicateClassifiers.isEmpty()) { + String msg = String.format( + "Duplicate classifiers detected after mapping. Duplicate classifiers=%s. " + + "Ensure all classifiers remain unique after mapping.", duplicateClassifiers); + log.error(msg); + throw new IllegalResourceStateException(msg, Source.builder().build()); + } + } + + protected List validateAndFilterExternalDb( + List externalDatabases, + ExternalDatabaseStrategy strategy, + FilterCriteria filterCriteria + ) { + if (isEmpty(externalDatabases)) return List.of(); - String externalNames = externalDatabases.stream() - .map(BackupExternalDatabase::getName) - .collect(Collectors.joining(", ")); + String externalNames = extractExternalDbName(externalDatabases, BackupExternalDatabase::getName); return switch (strategy) { case FAIL -> { - log.error("External databases not allowed by strategy={}: {}", ExternalDatabaseStrategy.FAIL, externalNames); + log.error("External databases not allowed by strategy={}. External db names: [{}]", + ExternalDatabaseStrategy.FAIL, externalNames); throw new DatabaseBackupRestoreNotSupportedException( - String.format("External databases not allowed by strategy=%s: %s", ExternalDatabaseStrategy.FAIL, externalNames), + String.format( + "External databases not allowed by strategy=%s. External db names: [%s]", + ExternalDatabaseStrategy.FAIL, externalNames + ), Source.builder().parameter("ExternalDatabaseStrategy").build() ); } case SKIP -> { - log.info("Excluding external databases from restore by strategy={}: external db names {}", + log.info("Excluding external databases from restore by strategy={}. External db names: [{}]", ExternalDatabaseStrategy.SKIP, externalNames); yield List.of(); } case INCLUDE -> { - log.info("Including external databases to restore by strategy: {}", ExternalDatabaseStrategy.INCLUDE); - if (isFilterEmpty(filterCriteria)) - yield mapper.toRestoreExternalDatabases(externalDatabases); - - Filter filter = filterCriteria.getFilter().getFirst(); + List restoreExternalDatabases; + if (isFilterEmpty(filterCriteria)) { + restoreExternalDatabases = externalDatabases.stream() + .map(db -> mapper.toRestoreExternalDatabase( + db, + db.getClassifiers().stream() + .map(c -> new ClassifierDetails(ClassifierType.NEW, db.getName(), null, new TreeMap<>(c))) + .toList() + )) + .toList(); + } else { + restoreExternalDatabases = 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.getInclude().stream().anyMatch(filter -> isMatches(filter, namespace, microserviceName, type, false)) + && filterCriteria.getExclude().stream().filter(this::isFilled).noneMatch(ex -> isMatches(ex, namespace, microserviceName, type, false));//isFilled + }) + .map(c -> new ClassifierDetails(ClassifierType.NEW, db.getName(), null, new TreeMap<>(c))) + .toList(); + + if (filteredClassifiers.isEmpty()) { + return null; + } - 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"); + return mapper.toRestoreExternalDatabase(db, filteredClassifiers); + }) + .filter(Objects::nonNull) + .toList(); } - String namespace = filter.getNamespace().getFirst(); - yield mapper.toRestoreExternalDatabases(externalDatabases).stream() - .filter(db -> db.getClassifiers().stream() - .anyMatch(classifier -> - namespace.equals(classifier.get(NAMESPACE))) - ).toList(); + log.info("Including external databases to restore by strategy={}. External db names: [{}]", + ExternalDatabaseStrategy.INCLUDE, extractExternalDbName(restoreExternalDatabases, RestoreExternalDatabase::getName)); + yield restoreExternalDatabases; } }; } - private List executeMappingForExternalDb(List externalDatabases, - Mapping mapping) { + private String extractExternalDbName(List externalDatabases, Function d) { return externalDatabases.stream() - .peek(db -> { - Set> uniqueClassifiers = new HashSet<>(); - List> updatedClassifiers = db.getClassifiers().stream() - .map(classifier -> updateAndValidateClassifier(classifier, mapping, uniqueClassifiers)) - .toList(); - db.setClassifiers(updatedClassifiers); - }) - .toList(); + .map(d) + .collect(Collectors.joining(", ")); + } + + + private List executeMappingForExternalDb( + List externalDatabases, + Mapping mapping + ) { + List updatedExternals = new ArrayList<>(); + + for (RestoreExternalDatabase externalDatabase : externalDatabases) { + List mappedClassifiers = applyMapping(externalDatabase.getClassifiers(), mapping); + externalDatabase.setClassifiers(mappedClassifiers); + updatedExternals.add(externalDatabase); + } + return updatedExternals; } private List createRestoreDatabases( - List backupDatabases + List backupDatabases ) { 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 = (String) classifiers.getFirst().getClassifier().get(NAMESPACE); String bgVersion = null; if (backupDatabase.isConfigurational()) { Optional bgNamespace = bgNamespaceRepository.findBgNamespaceByNamespace(namespace); @@ -883,137 +980,110 @@ private List createRestoreDatabases( } List users = backupDatabase.getUsers().stream() - .map(u -> new RestoreDatabase.User(u.getName(), u.getRole())) + .map(u -> new RestoreDatabase.User(null, u.getRole())) .toList(); - return RestoreDatabase.builder() - .backupDatabase(backupDatabase) - .name(backupDatabase.getName()) - .classifiers(classifiers) - .settings(backupDatabase.getSettings()) - .users(users) - .bgVersion(bgVersion) - .build(); + RestoreDatabase restoreDatabase = new RestoreDatabase(); + restoreDatabase.setId(UUID.randomUUID()); + restoreDatabase.setBackupDatabase(backupDatabase); + restoreDatabase.setName(backupDatabase.getName()); + restoreDatabase.setClassifiers(classifiers); + restoreDatabase.setSettings(backupDatabase.getSettings()); + restoreDatabase.setUsers(users); + restoreDatabase.setBgVersion(bgVersion); + + return restoreDatabase; }) .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)); - - SortedMap updatedClassifier = new TreeMap<>(classifier); - updatedClassifier.put(NAMESPACE, targetNamespace); - if (targetTenant != null) - updatedClassifier.put(TENANT_ID, targetTenant); - return updatedClassifier; - } - - private String getValue(Map map, String oldValue) { + private String applyMapping(Map map, String oldValue) { if (map == null || map.isEmpty()) { return oldValue; } return map.getOrDefault(oldValue, oldValue); } - private Map> groupBackupDatabasesByTypeAndAdapter( - List backupDatabases, - Mapping mapping) { - + private Map> groupBackupDatabasesByLogicalBackupNameAndAdapter( + List backupDatabases + ) { return backupDatabases.stream() - .map(db -> mapToPhysicalDatabaseEntry(db, mapping)) - .filter(Objects::nonNull) + .map(this::mapToAdapterBackupKeyEntry) .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(); - 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() - .filter(c -> mapping.getNamespaces().containsKey((String) c.get(NAMESPACE))) - .findFirst() - .orElse(classifiers.getFirst()); - - String oldNamespace = (String) matchedClassifier.get(NAMESPACE); - targetNamespace = mapping.getNamespaces().getOrDefault(oldNamespace, oldNamespace); - microserviceName = (String) matchedClassifier.get(MICROSERVICE_NAME); - } + private Map.Entry mapToAdapterBackupKeyEntry( + DatabaseWithClassifiers backupDatabaseDelegate + ) { + List classifiers = backupDatabaseDelegate.classifiers(); + String type = backupDatabaseDelegate.backupDatabase().getLogicalBackup().getType(); + String logicalBackupName = backupDatabaseDelegate.backupDatabase().getLogicalBackup().getLogicalBackupName(); - String type = db.backupDatabase().getLogicalBackup().getType(); + SortedMap firstNewClassifier = classifiers.getFirst().getClassifier(); + String targetNamespace = (String) firstNewClassifier.get(NAMESPACE); + String microserviceName = (String) firstNewClassifier.get(MICROSERVICE_NAME); PhysicalDatabase physicalDatabase = balancingRulesService .applyBalancingRules(type, targetNamespace, microserviceName); + String adapterId = physicalDatabase.getAdapter().getAdapterId(); // Checking adapter support backup restore - String adapterId = physicalDatabase.getAdapter().getAdapterId(); DbaasAdapter adapter = physicalDatabasesService.getAdapterById(adapterId); - if (!isBackupRestoreSupported(adapter)) { + if (isBackupRestoreUnsupported(adapter)) { throw new DatabaseBackupRestoreNotSupportedException( String.format("Adapter %s does not support restore operation", adapterId), Source.builder().build()); } - return Map.entry(physicalDatabase, new BackupDatabaseDelegate(db.backupDatabase(), classifiers)); - } - - private SortedMap updateAndValidateClassifier( - SortedMap classifier, - Mapping mapping, - Set> uniqueClassifiers) { - SortedMap updatedClassifier = updateClassifier(classifier, mapping); - // To prevent collision during mapping - if (!uniqueClassifiers.add(updatedClassifier)) { - String msg = String.format( - "Duplicate classifier detected after mapping: classifier='%s', mapping='%s'. " + - "Ensure all classifiers remain unique after mapping.", - classifier, mapping); - log.error(msg); - throw new IllegalResourceStateException(msg, Source.builder().build()); - } - return updatedClassifier; + return Map.entry(new AdapterBackupKey(adapterId, logicalBackupName), + new DatabaseWithClassifiers(backupDatabaseDelegate.backupDatabase(), classifiers)); } 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()); + log.info("Starting requesting adapters to restore startup process: restore={}, dryRun={}, logicalRestoreCount={}", + 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, storageName, blobPath, dryRun) ) .toList(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + private CompletableFuture runLogicalRestoreAsync(LogicalRestore logicalRestore, + String storageName, + String blobPath, + boolean dryRun + ) { + return CompletableFuture.supplyAsync( + asyncOperations.wrapWithContext( + () -> logicalRestore( + logicalRestore, + storageName, + blobPath, + dryRun + ) + ), + asyncOperations.getBackupPool() + ) + .thenAccept(response -> + refreshLogicalRestoreState(logicalRestore, response)) + .exceptionally(throwable -> { + logicalRestore.setStatus(RestoreTaskStatus.RETRYABLE_FAIL); + 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(), @@ -1037,7 +1107,7 @@ private void refreshLogicalRestoreState(LogicalRestore logicalRestore, LogicalRe restoreDatabase.setCreationTime(db.getCreationTime()); if (!restoreDatabase.getName().equals(db.getDatabaseName())) { restoreDatabase.setName(db.getDatabaseName()); - log.debug("For restore={} backup database updated: old name={}, new name={}", + log.debug("For restore={} restoreDatabase updated: old name={}, new name={}", logicalRestore.getRestore().getName(), db.getPreviousDatabaseName(), restoreDatabase.getName()); } } else { @@ -1058,43 +1128,41 @@ 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(LogicalRestore logicalRestore, + String storageName, + String blobPath, + boolean dryRun + ) { + List restoreDatabases = logicalRestore.getRestoreDatabases(); + 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(RESTORE_OPERATION, LOGICAL_RESTORE, logicalRestore.getId().toString(), logicalRestore.getAdapterId()); - 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 -> (String) c.getClassifier().get(NAMESPACE)) .findFirst() .orElse(""); String microserviceName = restoreDatabase.getClassifiers().stream() - .map(i -> (String) i.get(MICROSERVICE_NAME)) + .map(c -> (String) c.getClassifier().get(MICROSERVICE_NAME)) .findFirst() .orElse(""); return Map.of( MICROSERVICE_NAME, microserviceName, - DATABASE_NAME, restoreDatabase.getName(), + DATABASE_NAME, restoreDatabase.getBackupDatabase().getName(), NAMESPACE, namespace ); }) @@ -1104,7 +1172,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 +1182,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) { @@ -1133,49 +1202,66 @@ private LogicalRestoreAdapterResponse executeRestore( protected void checkRestoresAsync() { log.info("Starting restore scheduler"); LockAssert.assertLocked(); - List restoresToAggregate = restoreRepository.findRestoresToAggregate(); + List restoresToAggregate = restoreRepository.findRestoresToTrack(); - log.info("Founded restores to aggregate {}", + log.info("Found restores to aggregate {}", restoresToAggregate.stream().map(Restore::getName).toList()); - restoresToAggregate.forEach(this::trackAndAggregateRestore); restoresToAggregate.forEach(restore -> { - if (!Objects.equals(restore.getTotal(), restore.getCompleted()) && RestoreStatus.COMPLETED != restore.getStatus()) { + trackAndAggregateRestore(restore); + + if (RestoreStatus.COMPLETED != restore.getStatus()) { restoreRepository.save(restore); return; } - Map> dbNameToEnsuredUsers = restore.getLogicalRestores().stream() - .flatMap(lr -> lr.getRestoreDatabases().stream() - .map(rd -> Map.entry( - rd.getName(), - ensureUsers(lr.getAdapterId(), rd.getName(), rd.getUsers()) - )) - ) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - initializeLogicalDatabasesFromRestore(restore, dbNameToEnsuredUsers); - restoreRepository.save(restore); + try { + Map> dbNameToEnsuredUsers = restore.getLogicalRestores().stream() + .flatMap(lr -> lr.getRestoreDatabases().stream() + .map(rd -> Map.entry( + rd.getName(), + ensureUsers(lr.getAdapterId(), rd.getId().toString(), rd.getName(), rd.getUsers()) + )) + ) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a)); + + createLogicalDatabasesFromRestore(restore, dbNameToEnsuredUsers); + restoreRepository.save(restore); + } catch (Exception e) { + log.error("Exception occurred during restore process", e); + restore.setStatus(RestoreStatus.FAILED); + restore.setErrorMessage(extractErrorMessage(e)); + restoreRepository.save(restore); + throw e; + } }); } protected List ensureUsers(String adapterId, + String dbId, String dbName, List users) { DbaasAdapter adapter = physicalDatabasesService.getAdapterById(adapterId); - RetryPolicy retryPolicy = buildRetryPolicy(dbName, ENSURE_USER_OPERATION); + RetryPolicy retryPolicy = buildRetryPolicy(ENSURE_USER_OPERATION, RESTORE_DATABASE, dbId, adapterId); - log.info("Ensuring {} users for databaseName=[{}] via adapter [{}]", - users.size(), dbName, adapterId); + log.info("Start ensuring {} users for database=[{}] via adapter [{}]", + users.size(), dbId, 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(null, null, dbName, user.getRole()); + user.setName(ensuredUser.getName()); + log.info("User ensured for database=[{}], user=[name:{}, connectionProperties:{}]", + dbId, ensuredUser.getName(), ensuredUser.getConnectionProperties()); + return ensuredUser; + }); } catch (Exception e) { - log.error("Failed to ensure user {} in database {}", user.getName(), dbName, e); + log.error("Failed to ensure user in database [{}] with role [{}]", dbId, user.getRole()); throw new BackupExecutionException( - String.format("Failed to ensure user %s", user.getName()), e); + String.format("Failed to ensure user in database [%s] with role [%s]", dbId, user.getRole()), e); } }) .toList(); @@ -1184,7 +1270,7 @@ protected List ensureUsers(String adapterId, protected void trackAndAggregateRestore(Restore restore) { if (restore.getAttemptCount() > retryCount) { - log.warn("The number of attempts of track restore {} exceeded {}", restore.getName(), retryCount); + log.warn("The number of attempts to track restore {} exceeded {}", restore.getName(), retryCount); restore.setStatus(RestoreStatus.FAILED); restore.setErrorMessage(String.format("The number of attempts exceeded %s", retryCount)); } else { @@ -1197,26 +1283,36 @@ protected void trackAndAggregateRestore(Restore restore) { private void fetchStatuses(Restore restore) { List notFinishedLogicalRestores = restore.getLogicalRestores().stream() - .filter(db -> RestoreTaskStatus.IN_PROGRESS == db.getStatus() - || RestoreTaskStatus.NOT_STARTED == db.getStatus()) - .toList(); + .filter(db -> + db.getStatus() == RestoreTaskStatus.IN_PROGRESS + || db.getStatus() == NOT_STARTED + || db.getStatus() == RestoreTaskStatus.RETRYABLE_FAIL + ).toList(); log.debug("Starting checking status for logical restores: restore={}, logicalRestores={}", restore.getName(), notFinishedLogicalRestores.stream() - .map(LogicalRestore::getLogicalRestoreName) + .map(LogicalRestore::getId) .toList()); List> futures = notFinishedLogicalRestores.stream() .map(logicalRestore -> { - RetryPolicy retryPolicy = buildRetryPolicy(logicalRestore.getLogicalRestoreName(), TRACK_RESTORE_OPERATION); + RetryPolicy retryPolicy = buildRetryPolicy(TRACK_RESTORE_OPERATION, LOGICAL_RESTORE, logicalRestore.getId().toString(), logicalRestore.getAdapterId()); return CompletableFuture.supplyAsync( - asyncOperations.wrapWithContext(() -> Failsafe.with(retryPolicy).get(() -> { - DbaasAdapter adapter = physicalDatabasesService.getAdapterById(logicalRestore.getAdapterId()); - return adapter.trackRestoreV2(logicalRestore.getLogicalRestoreName(), restore.getStorageName(), restore.getBlobPath()); - }))) + asyncOperations.wrapWithContext( + () -> Failsafe.with(retryPolicy).get(() -> { + DbaasAdapter adapter = physicalDatabasesService.getAdapterById(logicalRestore.getAdapterId()); + if (isLogicalRestoreNotStarted(logicalRestore)) { + LogicalRestoreAdapterResponse response = logicalRestore(logicalRestore, restore.getStorageName(), restore.getBlobPath(), false); + refreshLogicalRestoreState(logicalRestore, response); + } + return adapter.trackRestoreV2(logicalRestore.getLogicalRestoreName(), restore.getStorageName(), restore.getBlobPath()); + } + ) + ), asyncOperations.getBackupPool()) .thenAccept(response -> refreshLogicalRestoreState(logicalRestore, response)) .exceptionally(throwable -> { + logicalRestore.setStatus(RestoreTaskStatus.RETRYABLE_FAIL); logicalRestore.setErrorMessage(throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage()); return null; @@ -1234,7 +1330,6 @@ private void aggregateRestoreStatus(Restore restore) { int totalDbCount = 0; int countCompletedDb = 0; - long totalDuration = 0; List errorMessages = new ArrayList<>(); for (LogicalRestore lr : logicalRestoreList) { @@ -1253,106 +1348,130 @@ private void aggregateRestoreStatus(Restore restore) { for (RestoreDatabase restoreDatabase : dbs) { totalDbCount += 1; countCompletedDb += RestoreTaskStatus.COMPLETED == restoreDatabase.getStatus() ? 1 : 0; - totalDuration += restoreDatabase.getDuration(); } } - restore.setStatus(aggregateRestoreStatus(statusSet)); + restore.setStatus(aggregateRestoreTaskStatus(statusSet)); restore.setTotal(totalDbCount); restore.setCompleted(countCompletedDb); - restore.setDuration(totalDuration); restore.setErrorMessage(String.join("; ", errorMessages)); log.info("Aggregated restore status: restoreName={}, status={}, totalDb={}, completed={}, errorMessage={}", restore.getName(), restore.getStatus(), restore.getTotal(), restore.getCompleted(), restore.getErrorMessage()); } @Transactional - protected void initializeLogicalDatabasesFromRestore(Restore restore, Map> dbNameToEnsuredUsers) { - log.info("Start creating logicalDatabases from restore {}", restore.getName()); - try { - // Creating LogicalDb based logicalRestores - restore.getLogicalRestores().forEach(logicalRestore -> { - log.info("Processing logicalRestore={}, type={}, adapterId={}", logicalRestore.getLogicalRestoreName(), logicalRestore.getType(), logicalRestore.getAdapterId()); - logicalRestore.getRestoreDatabases().forEach(restoreDatabase -> { - String type = logicalRestore.getType(); - Set> classifiers = new HashSet<>(); - - log.info("Processing restoreDatabase={}", restoreDatabase.getName()); - findSimilarDbByClassifier(classifiers, restoreDatabase.getClassifiers(), type); - String adapterId = logicalRestore.getAdapterId(); - String physicalDatabaseId = physicalDatabasesService.getByAdapterId(adapterId).getPhysicalDatabaseIdentifier(); - List ensuredUsers = dbNameToEnsuredUsers.get(restoreDatabase.getName()); - Database newDatabase = createLogicalDatabase( - restoreDatabase.getName(), - restoreDatabase.getSettings(), - classifiers, - type, - false, - false, - adapterId, - physicalDatabaseId, - restoreDatabase.getBgVersion()); - - newDatabase.setConnectionProperties(ensuredUsers.stream().map(EnsuredUser::getConnectionProperties).toList()); - newDatabase.setResources(ensuredUsers.stream().map(EnsuredUser::getResources).filter(Objects::nonNull).flatMap(Collection::stream).toList()); - newDatabase.setResources(newDatabase.getResources().stream().distinct().collect(Collectors.toList())); - encryption.encryptPassword(newDatabase); - databaseRegistryDbaasRepository.saveInternalDatabase(newDatabase.getDatabaseRegistry().getFirst()); - log.info("Based restoreDatabase={}, database id={} created", restore.getName(), newDatabase.getId()); - }); - }); - // Creating LogicalDb based externalDbs - restore.getExternalDatabases().forEach(externalDatabase -> { - log.info("Processing externalDatabase={}, type={}", externalDatabase.getName(), externalDatabase.getType()); - String type = externalDatabase.getType(); - Set> classifiers = new HashSet<>(); - - findSimilarDbByClassifier(classifiers, externalDatabase.getClassifiers(), type); + protected void createLogicalDatabasesFromRestore(Restore restore, + Map> dbNameToEnsuredUsers) { + log.info("Start creating logical databases from restore {}", restore.getName()); + // Creating LogicalDb based logicalRestores + restore.getLogicalRestores().forEach(logicalRestore -> { + log.info("Processing logicalRestore={}, type={}, adapterId={}", logicalRestore.getLogicalRestoreName(), logicalRestore.getType(), logicalRestore.getAdapterId()); + logicalRestore.getRestoreDatabases().forEach(restoreDatabase -> { + String type = logicalRestore.getType(); + log.info("Processing restoreDatabase={}", restoreDatabase.getName()); + findAndMarkDatabaseAsOrphan(restoreDatabase.getClassifiers(), type); + String adapterId = logicalRestore.getAdapterId(); + String physicalDatabaseId = physicalDatabasesService.getByAdapterId(adapterId).getPhysicalDatabaseIdentifier(); + List ensuredUsers = dbNameToEnsuredUsers.get(restoreDatabase.getName()); Database newDatabase = createLogicalDatabase( - externalDatabase.getName(), - null, - classifiers, + restoreDatabase.getName(), + restoreDatabase.getSettings(), + restoreDatabase.getClassifiers().stream() + .map(ClassifierDetails::getClassifier).collect(Collectors.toSet()), type, - true, - true, - null, - null, - null); - databaseRegistryDbaasRepository.saveExternalDatabase(newDatabase.getDatabaseRegistry().getFirst()); - log.info("Based externalDb={}, database id={} created", externalDatabase.getName(), newDatabase.getId()); + false, + false, + adapterId, + physicalDatabaseId, + restoreDatabase.getBgVersion()); + + newDatabase.setConnectionProperties(ensuredUsers.stream().map(EnsuredUser::getConnectionProperties).toList()); + newDatabase.setResources(ensuredUsers.stream().map(EnsuredUser::getResources).filter(Objects::nonNull).flatMap(Collection::stream).toList()); + newDatabase.setResources(newDatabase.getResources().stream().distinct().collect(Collectors.toList())); + encryption.encryptPassword(newDatabase); + databaseRegistryDbaasRepository.saveInternalDatabase(newDatabase.getDatabaseRegistry().getFirst()); + log.info("Based on restoreDatabase={}, database with id={} created", restoreDatabase.getName(), newDatabase.getId()); }); - restore.setStatus(RestoreStatus.COMPLETED); - log.info("Finished initializing logical databases from restore {}", restore.getName()); - } catch (Exception e) { - log.error("Exception occurred during restore process", e); - restore.setStatus(RestoreStatus.FAILED); - restore.setErrorMessage(e.getMessage()); - throw e; - } + }); + // Creating LogicalDb based externalDbs + restore.getExternalDatabases().forEach(externalDatabase -> { + log.info("Processing externalDatabase={}, type={}", externalDatabase.getName(), externalDatabase.getType()); + String type = externalDatabase.getType(); + findAndMarkDatabaseAsOrphan(externalDatabase.getClassifiers(), type); + Database newDatabase = createLogicalDatabase( + externalDatabase.getName(), + null, + externalDatabase.getClassifiers().stream() + .map(ClassifierDetails::getClassifier).collect(Collectors.toSet()), + type, + true, + true, + null, + null, + null); + databaseRegistryDbaasRepository.saveExternalDatabase(newDatabase.getDatabaseRegistry().getFirst()); + log.info("Based on externalDb={}, database with id={} created", externalDatabase.getName(), newDatabase.getId()); + }); + restore.setStatus(RestoreStatus.COMPLETED); + log.info("Finished initializing logical databases from restore {}", restore.getName()); } - private void findSimilarDbByClassifier(Set> uniqueClassifiers, - List> classifiers, - String type) { + private Set findSimilarDbByClassifier(List classifiers, String type) { + 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(); + log.debug("Classifier candidate: {}", currClassifier); + + Optional optionalDatabaseRegistry = databaseRegistryDbaasRepository + .getDatabaseByClassifierAndType(currClassifier, type); + + if (optionalDatabaseRegistry.isPresent()) { + DatabaseRegistry dbRegistry = optionalDatabaseRegistry.get(); + Database db = dbRegistry.getDatabase(); + classifier.setType(ClassifierType.REPLACED); + classifier.setPreviousDatabase(db.getName()); + log.info("Found existing database {} for classifier {}", db.getId(), currClassifier); + Set existClassifiers = db.getDatabaseRegistry().stream() + .map(AbstractDatabaseRegistry::getClassifier) + .filter(c -> !c.containsKey(MARKED_FOR_DROP)) + .map(TreeMap::new) + .map(c -> currClassifier.equals(c) + ? classifier + : new ClassifierDetails(ClassifierType.TRANSIENT_REPLACED, db.getName(), new TreeMap<>(c), null) + ) + .collect(Collectors.toSet()); + + uniqueClassifiers.addAll(existClassifiers); + } else + uniqueClassifiers.add(classifier); }); + return uniqueClassifiers; + } + + private void findAndMarkDatabaseAsOrphan(List classifiers, String type) { + classifiers.stream() + .filter(classifier -> ClassifierType.REPLACED == classifier.getType() || + ClassifierType.TRANSIENT_REPLACED == classifier.getType() + ) + .forEach(classifier -> { + SortedMap currClassifier = classifier.getClassifier(); + databaseRegistryDbaasRepository + .getDatabaseByClassifierAndType(currClassifier, type) + .ifPresentOrElse(dbRegistry -> { + dBaaService.markDatabasesAsOrphan(dbRegistry); + databaseRegistryDbaasRepository.saveAnyTypeLogDb(dbRegistry); + log.info( + "Database marked as orphan: dbId={}, dbType={}, classifier={}", + dbRegistry.getDatabase().getId(), + type, + currClassifier + ); + }, () -> log.debug("Database not found for classifier: dbType={}, classifierType={}, classifier={}", + type, + classifier.getType(), + currClassifier) + ); + }); } private Database createLogicalDatabase(String dbName, @@ -1430,13 +1549,56 @@ public void deleteRestore(String restoreName) { restoreRepository.save(restore); } - public RestoreResponse retryRestore(String restoreName) { - throw new FunctionalityNotImplemented("retry restore functionality not implemented yet"); + public RestoreResponse retryRestore(String restoreName, boolean allowParallel) { + Restore restore = getRestoreOrThrowException(restoreName); + + 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()); + } + + Restore retriedRestore = restoreLockWrapper(() -> { + retryRestore(restore); + aggregateRestoreStatus(restore); + return restoreRepository.save(restore); + }, allowParallel); + + return mapper.toRestoreResponse(retriedRestore); } - protected Map> validateAndFilterDatabasesForBackup(Map> databasesForBackup, - boolean ignoreNotBackupableDatabases, - ExternalDatabaseStrategy strategy) { + private void checkBackupStatusForRestore(String restoreName, BackupStatus status) { + if (status != BackupStatus.COMPLETED) { + log.error("Restore {} can't be processed due to backup status {}", restoreName, status); + throw new UnprocessableEntityException( + restoreName, String.format("Restore can't be processed due to backup status %s", status), + Source.builder().build()); + } + } + + private void retryRestore(Restore restore) { + restore.resetAttempt(); + restore.setStatus(RestoreStatus.IN_PROGRESS); + restore.getLogicalRestores().stream() + .filter(logicalRestore -> + RestoreTaskStatus.FAILED == logicalRestore.getStatus() + || logicalRestore.getLogicalRestoreName() == null + || logicalRestore.getLogicalRestoreName().isEmpty() + ) + .forEach(logicalRestore -> { + logicalRestore.setStatus(RestoreTaskStatus.RETRYABLE_FAIL); + logicalRestore.setLogicalRestoreName(null); + }); + } + + protected Map> validateAndFilterDatabasesForBackup( + Map> databasesForBackup, + boolean ignoreNotBackupableDatabases, + ExternalDatabaseStrategy strategy) { Map>> partitioned = databasesForBackup.entrySet().stream() .collect(Collectors.groupingBy(entry -> @@ -1444,10 +1606,10 @@ protected Map> validateAndFilterDatabasesForBac Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue) )); - Map> externalDatabases = partitioned.get(true); - Map> internalDatabases = partitioned.get(false); + Map> externalDatabases = partitioned.getOrDefault(true, Map.of()); + Map> internalDatabases = partitioned.getOrDefault(false, Map.of()); - if (externalDatabases != null && !externalDatabases.isEmpty()) { + if (!externalDatabases.isEmpty()) { String externalNames = externalDatabases.keySet().stream() .map(Database::getName) .collect(Collectors.joining(", ")); @@ -1474,7 +1636,7 @@ protected Map> validateAndFilterDatabasesForBac return true; } if (db.getAdapterId() != null) { - return !isBackupRestoreSupported(physicalDatabasesService.getAdapterById(db.getAdapterId())); + return isBackupRestoreUnsupported(physicalDatabasesService.getAdapterById(db.getAdapterId())); } return false; }) @@ -1508,8 +1670,13 @@ protected Map> validateAndFilterDatabasesForBac return filteredDatabases; } - private boolean isBackupRestoreSupported(DbaasAdapter adapter) { - return adapter.isBackupRestoreSupported(); + @Transactional + public void deleteBackupFromDb(String backupName) { + backupRepository.deleteById(backupName); + } + + private boolean isBackupRestoreUnsupported(DbaasAdapter adapter) { + return !adapter.isBackupRestoreSupported(); } private void backupExistenceCheck(String backupName) { @@ -1529,15 +1696,46 @@ private Restore getRestoreOrThrowException(String restoreName) { .orElseThrow(() -> new BackupRestorationNotFoundException(restoreName, Source.builder().build())); } - private RetryPolicy buildRetryPolicy(String name, String operation) { + private T restoreLockWrapper(Supplier action, boolean allowParallel) { + if (allowParallel) + return action.get(); + + // Only one restore operation able to process + LockConfiguration config = new LockConfiguration( + Instant.now(), + RESTORE, + Duration.ofMinutes(LOCK_AT_MOST), + Duration.ofMinutes(LOCK_AT_LEAST)); + + Optional optLock = lockProvider.lock(config); + + if (optLock.isEmpty()) + throw new OperationAlreadyRunningException("restore"); + // Start locking action + SimpleLock lock = optLock.get(); + try { + if (restoreRepository.countNotCompletedRestores() > 0) + throw new OperationAlreadyRunningException("restore"); + return action.get(); + } finally { + try { + lock.unlock(); + } catch (IllegalStateException ex) { + log.debug("Lock is already unlocked", ex); + } + } + } + + private RetryPolicy buildRetryPolicy(String operation, String entityType, String entityId, String adapterId) { + String context = String.format("%s operation [%s=%s, adapter=%s]", operation, entityType, entityId, adapterId); return new RetryPolicy<>() .handle(WebApplicationException.class) .withMaxRetries(retryAttempts) .withDelay(retryDelay) .onFailedAttempt(e -> log.warn("Attempt failed for {}: {}", - name, extractErrorMessage(e.getLastFailure()))) - .onRetry(e -> log.info("Retrying {}...", operation)) - .onFailure(e -> log.error("Request limit exceeded for {}", name)); + context, extractErrorMessage(e.getLastFailure()))) + .onRetry(e -> log.info("Retrying {}...", context)) + .onFailure(e -> log.error("Request limit exceeded for {}", context)); } private String extractErrorMessage(Throwable throwable) { @@ -1557,39 +1755,48 @@ private String extractErrorMessage(Throwable throwable) { } private boolean isFilterEmpty(FilterCriteria filterCriteria) { - if (filterCriteria == null || filterCriteria.getFilter() == null) - return true; + return filterCriteria == null + || (isEmpty(filterCriteria.getInclude()) && isEmpty(filterCriteria.getExclude())); + } - return filterCriteria.getFilter().isEmpty() - || filterCriteria.getFilter().stream().allMatch(this::isSingleFilterEmpty); + private boolean isEmpty(Collection c) { + return c == null || c.isEmpty(); } - private boolean isSingleFilterEmpty(Filter f) { - return (f.getNamespace() == null || f.getNamespace().isEmpty()) - && (f.getMicroserviceName() == null || f.getMicroserviceName().isEmpty()) - && (f.getDatabaseType() == null || f.getDatabaseType().isEmpty()) - && (f.getDatabaseKind() == null || f.getDatabaseKind().isEmpty()); + private boolean isLogicalBackupNotStarted(LogicalBackup logicalBackup) { + return logicalBackup.getLogicalBackupName() == null || logicalBackup.getLogicalBackupName().isBlank(); } - private static , R extends Enum> R aggregateStatus( - Set statusSet, - Function taskStatusGetter, - Function resultStatusGetter) { + private boolean isLogicalRestoreNotStarted(LogicalRestore logicalRestore) { + return logicalRestore.getLogicalRestoreName() == null || logicalRestore.getLogicalRestoreName().isBlank(); + } - if (statusSet.contains(taskStatusGetter.apply("NOT_STARTED")) && statusSet.size() == 1) - return resultStatusGetter.apply("NOT_STARTED"); + protected BackupStatus aggregateBackupTaskStatus(Set backupTaskStatuses) { + if (backupTaskStatuses.contains(BackupTaskStatus.NOT_STARTED) || + backupTaskStatuses.contains(BackupTaskStatus.RETRYABLE_FAIL) || + backupTaskStatuses.contains(BackupTaskStatus.IN_PROGRESS)) + return BackupStatus.IN_PROGRESS; - if (statusSet.contains(taskStatusGetter.apply("NOT_STARTED")) && statusSet.size() > 1) - return resultStatusGetter.apply("IN_PROGRESS"); + if (backupTaskStatuses.contains(BackupTaskStatus.FAILED)) + return BackupStatus.FAILED; - if (statusSet.contains(taskStatusGetter.apply("IN_PROGRESS"))) - return resultStatusGetter.apply("IN_PROGRESS"); + if (backupTaskStatuses.contains(BackupTaskStatus.COMPLETED)) + return BackupStatus.COMPLETED; + return null; + } + + protected RestoreStatus aggregateRestoreTaskStatus(Set backupTaskStatuses) { + if (backupTaskStatuses.contains(RestoreTaskStatus.NOT_STARTED) || + backupTaskStatuses.contains(RestoreTaskStatus.RETRYABLE_FAIL) || + backupTaskStatuses.contains(RestoreTaskStatus.IN_PROGRESS)) + return RestoreStatus.IN_PROGRESS; - if (statusSet.contains(taskStatusGetter.apply("FAILED"))) - return resultStatusGetter.apply("FAILED"); + if (backupTaskStatuses.contains(RestoreTaskStatus.FAILED)) + return RestoreStatus.FAILED; - if (statusSet.contains(taskStatusGetter.apply("COMPLETED"))) - return resultStatusGetter.apply("COMPLETED"); + if (backupTaskStatuses.contains(RestoreTaskStatus.COMPLETED)) + return RestoreStatus.COMPLETED; return null; } + } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/DigestUtil.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/DigestUtil.java index 3f35f5d7..3ae5968e 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/DigestUtil.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/utils/DigestUtil.java @@ -9,6 +9,7 @@ import com.netcracker.cloud.dbaas.exceptions.DigestCalculationException; import lombok.extern.slf4j.Slf4j; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; @@ -29,7 +30,7 @@ public static String calculateDigest(Object obj) { String json = OBJECT_MAPPER.writeValueAsString(obj); MessageDigest digest = MessageDigest.getInstance(ALGORITHM); - byte[] hash = digest.digest(json.getBytes()); + byte[] hash = digest.digest(json.getBytes(StandardCharsets.UTF_8)); String base64Hash = Base64.getEncoder().encodeToString(hash); return ALGORITHM + "=" + base64Hash; 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/main/resources/db/migration/postgresql/V1.034__Backup_Restore_Not_Null_Statuses.sql b/dbaas/dbaas-aggregator/src/main/resources/db/migration/postgresql/V1.034__Backup_Restore_Not_Null_Statuses.sql new file mode 100644 index 00000000..c360a4ec --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/resources/db/migration/postgresql/V1.034__Backup_Restore_Not_Null_Statuses.sql @@ -0,0 +1,13 @@ +UPDATE backup SET status = 'FAILED' WHERE status IS NULL; +UPDATE backup_logical SET status = 'FAILED' WHERE status IS NULL; +UPDATE backup_database SET status = 'FAILED' WHERE status IS NULL; +UPDATE restore SET status = 'FAILED' WHERE status IS NULL; +UPDATE restore_logical SET status = 'FAILED' WHERE status IS NULL; +UPDATE restore_database SET status = 'FAILED' WHERE status IS NULL; + +ALTER TABLE backup ALTER COLUMN status SET NOT NULL; +ALTER TABLE backup_logical ALTER COLUMN status SET NOT NULL; +ALTER TABLE backup_database ALTER COLUMN status SET NOT NULL; +ALTER TABLE restore ALTER COLUMN status SET NOT NULL; +ALTER TABLE restore_logical ALTER COLUMN status SET NOT NULL; +ALTER TABLE restore_database ALTER COLUMN status SET NOT NULL; 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..e3606488 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 @@ -23,10 +23,7 @@ import org.mockito.Mockito; import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import static io.restassured.RestAssured.given; import static jakarta.ws.rs.core.Response.Status.*; @@ -86,7 +83,7 @@ void initiateBackup_invalidDto() { .statusCode(BAD_REQUEST.getStatusCode()) .body("message", allOf( containsString("backupName: must not be blank"), - containsString("filter: must not be null") + containsString("include: there should be at least one filter specified") )); verify(dbBackupV2Service, times(0)).backup(any(), anyBoolean()); @@ -111,8 +108,8 @@ void initiateBackup_databasesCannotBackup_ignoreNotBackupableDatabaseFalse() { .when().post("/backup") .then() .statusCode(422) - .body("reason", equalTo("Backup not allowed")) - .body("message", equalTo("The backup/restore request can`t be processed. Backup operation unsupported for databases: " + dbNames)); + .body("reason", equalTo("Operation not allowed")) + .body("message", equalTo("The backup/restore request can't be processed. Backup operation unsupported for databases: " + dbNames)); verify(dbBackupV2Service, times(1)).backup(backupRequest, false); } @@ -139,6 +136,58 @@ 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.setInclude(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", allOf( + containsString("exclude[0]: Filter must have at least one non-null field"), + containsString("include[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.setInclude(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", allOf( + containsString("exclude[0]: Filter must have at least one non-null field"), + containsString("include[0]: Filter must have at least one non-null field") + ) + ); + verify(dbBackupV2Service, times(0)).restore(backupName, restoreRequest, false, false); + } + @Test void getBackupStatus_validBackupNameCase() { String backupName = "test-backup-name"; @@ -262,6 +311,10 @@ void uploadMetadata_DigestHeaderAndBodyNotEqual() { backupResponse.setBlobPath("path"); backupResponse.setStorageName("storageName"); backupResponse.setExternalDatabaseStrategy(ExternalDatabaseStrategy.SKIP); + backupResponse.setTotal(0); + backupResponse.setSize(0L); + backupResponse.setCompleted(0); + String expectedDigest = DigestUtil.calculateDigest(backupResponse); String incomingDigest = "SHA-256=abc"; given().auth().preemptive().basic("backup_manager", "backup_manager") @@ -309,7 +362,7 @@ void deleteBackup_shouldReturn409_onIllegalState() { .then() .statusCode(422) .body("message", - equalTo(String.format("Resource '%s' can`t be processed: %s", backupName, + equalTo(String.format("Resource '%s' can't be processed: %s", backupName, "has invalid status '" + backupStatus + "'. Only COMPLETED or FAILED backups can be processed."))); } @@ -318,7 +371,7 @@ public static BackupRequest createBackupRequest(String namespace, String backupN filter.setNamespace(List.of(namespace)); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); BackupRequest dto = new BackupRequest(); dto.setFilterCriteria(filterCriteria); @@ -330,12 +383,29 @@ 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.setInclude(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<>(); sortedMap.put("key", "value"); BackupDatabaseResponse backupDatabaseResponse = new BackupDatabaseResponse( + UUID.randomUUID(), "backup-database", List.of(sortedMap), Map.of("settings-key", "settings-value"), @@ -353,6 +423,7 @@ private BackupResponse createBackupResponse(String backupName) { ); LogicalBackupResponse logicalBackupResponse = new LogicalBackupResponse( + UUID.randomUUID(), "logicalBackupName", "adapterID", "type", @@ -374,7 +445,7 @@ private BackupResponse createBackupResponse(String backupName) { filter.setNamespace(List.of("namespace")); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); SortedMap map = new TreeMap<>(); map.put("key", "value"); 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..bad5f8b1 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.DatabaseWithClassifiers; 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.*; @@ -22,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.core.LockAssert; import net.javacrumbs.shedlock.core.LockProvider; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -93,6 +95,8 @@ class DbBackupV2ServiceTest { @Inject @Named("process-orchestrator") private DataSource dataSource; + @InjectMock + private DbaaSHelper dbaaSHelper; @BeforeEach void setUp() { @@ -282,8 +286,8 @@ void backup_backupFinishedStatusCompletedExternalDatabaseStrategyInclude() { FilterCriteriaEntity backupFilter = backup.getFilterCriteria(); assertNotNull(backupFilter); - assertEquals(1, backupFilter.getFilter().size()); - assertEquals(namespace, backupFilter.getFilter().getFirst().getNamespace().getFirst()); + assertEquals(1, backupFilter.getInclude().size()); + assertEquals(namespace, backupFilter.getInclude().getFirst().getNamespace().getFirst()); List externalDatabases = backup.getExternalDatabases(); assertNotNull(externalDatabases); @@ -734,7 +738,7 @@ void backup_dryRunTrue() { "tenant".equals(classifier.get(SCOPE)) && tenantId.equals(classifier.get(TENANT_ID))); BackupDatabaseResponse.User user = backupDatabase.getUsers().getFirst(); - assertEquals("username", user.getName()); + assertEquals("oldUsername", user.getName()); assertEquals("admin", user.getRole()); Backup backup = backupRepository.findById(backupName); @@ -880,18 +884,17 @@ void restore_withoutMapping_finishedWithStatusCompleted() { user2.setConnectionProperties(Map.of()); user2.setResources(List.of(resource2)); - when(dbaasAdapter1.ensureUser("username", null, newName1, "admin")).thenReturn(user1); - when(dbaasAdapter2.ensureUser("username", null, newName2, "admin")).thenReturn(user2); + when(dbaasAdapter1.ensureUser(null, null, newName1, "admin")).thenReturn(user1); + when(dbaasAdapter2.ensureUser(null, null, newName2, "admin")).thenReturn(user2); RestoreResponse restoreResponse = dbBackupV2Service.restore( backupName, getRestoreRequest(restoreName, List.of(namespace), ExternalDatabaseStrategy.FAIL, null, null), - false); + false, false); assertNotNull(restoreResponse); assertEquals(restoreName, restoreResponse.getRestoreName()); assertEquals(RestoreStatus.IN_PROGRESS, restoreResponse.getStatus()); - assertEquals(2, restoreResponse.getDuration()); dbBackupV2Service.checkRestoresAsync(); Restore restore = restoreRepository.findById(restoreName); @@ -905,7 +908,6 @@ void restore_withoutMapping_finishedWithStatusCompleted() { assertEquals(2, restore.getTotal()); assertEquals(2, restore.getCompleted()); assertTrue(restore.getErrorMessage().isBlank()); - assertEquals(2, restore.getDuration()); assertEquals(1, restore.getAttemptCount()); assertEquals(2, restore.getLogicalRestores().size()); assertEquals(0, restore.getExternalDatabases().size()); @@ -929,7 +931,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 +956,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)) @@ -1157,8 +1159,8 @@ void restore_withMapping_finishedWithStatusCompleted() { user2.setConnectionProperties(Map.of()); user2.setResources(List.of(resource2)); - when(dbaasAdapter1.ensureUser("username", null, newName1, "admin")).thenReturn(user1); - when(dbaasAdapter2.ensureUser("username", null, newName2, "admin")).thenReturn(user2); + when(dbaasAdapter1.ensureUser(null, null, newName1, "admin")).thenReturn(user1); + when(dbaasAdapter2.ensureUser(null, null, newName2, "admin")).thenReturn(user2); Map namespaceMap = Map.of(namespace, mappedNamespace); @@ -1167,12 +1169,11 @@ void restore_withMapping_finishedWithStatusCompleted() { RestoreResponse restoreResponse = dbBackupV2Service.restore( backupName, getRestoreRequest(restoreName, List.of(namespace), ExternalDatabaseStrategy.INCLUDE, namespaceMap, tenantMap), - false); + false, false); assertNotNull(restoreResponse); assertEquals(restoreName, restoreResponse.getRestoreName()); assertEquals(RestoreStatus.IN_PROGRESS, restoreResponse.getStatus()); - assertEquals(2, restoreResponse.getDuration()); dbBackupV2Service.checkRestoresAsync(); Restore restore = restoreRepository.findById(restoreName); @@ -1186,7 +1187,6 @@ void restore_withMapping_finishedWithStatusCompleted() { assertEquals(2, restore.getTotal()); assertEquals(2, restore.getCompleted()); assertTrue(restore.getErrorMessage().isBlank()); - assertEquals(2, restore.getDuration()); assertEquals(1, restore.getAttemptCount()); assertEquals(2, restore.getLogicalRestores().size()); assertEquals(1, restore.getExternalDatabases().size()); @@ -1195,7 +1195,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 +1221,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 +1247,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 +1344,8 @@ 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'. " + - "Ensure all classifiers remain unique after mapping.", - classifier2, mapping); BackupDatabase backupDatabase = getBackupDatabase(dbName, List.of(classifier1, classifier2), false, BackupTaskStatus.COMPLETED, ""); LogicalBackup logicalBackup = getLogicalBackup(logicalBackupName, adapterId, postgresType, List.of(backupDatabase), BackupTaskStatus.COMPLETED, ""); @@ -1374,10 +1371,11 @@ void restore_mappingInvokeCollision() { IllegalResourceStateException ex = assertThrows(IllegalResourceStateException.class, () -> dbBackupV2Service.restore(backupName, getRestoreRequest(restoreName, List.of(namespace), ExternalDatabaseStrategy.FAIL, mapping.getNamespaces(), mapping.getTenants()), - false + false, false )); - assertEquals(msg, ex.getDetail()); + String msg = mappedClassifier.toString(); + assertTrue(ex.getDetail().contains(msg)); } @Test @@ -1434,7 +1432,7 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { physicalDatabase1.setType(postgresType); physicalDatabase1.setPhysicalDatabaseIdentifier("postgres-dev"); - when(balancingRulesService.applyBalancingRules(postgresType, mappedNamespace, microserviceName1)) + when(balancingRulesService.applyBalancingRules(eq(postgresType), anyString(), anyString())) .thenReturn(physicalDatabase1); when(physicalDatabasesService.getByAdapterId(adapterId)).thenReturn(physicalDatabase1); @@ -1474,24 +1472,27 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { DbResource resource1 = new DbResource(); resource1.setId(UUID.randomUUID()); resource1.setKind("kind"); - resource1.setName("name"); + resource1.setName("newName"); EnsuredUser user1 = new EnsuredUser(); user1.setConnectionProperties(Map.of( - "key", "value" + "username", "newName" )); user1.setResources(List.of(resource1)); + user1.setName("newName"); - when(dbaasAdapter.ensureUser("username", null, newName, "admin")).thenReturn(user1); + when(dbaasAdapter.ensureUser(null, null, newName, "admin")).thenReturn(user1); Map namespaceMap = Map.of(namespace, mappedNamespace); Map tenantMap = Map.of(tenantId, mappedTenantId); dbBackupV2Service.restore(backupName, getRestoreRequest(restoreName, List.of(namespace), ExternalDatabaseStrategy.INCLUDE, namespaceMap, tenantMap), - false + false, false ); dbBackupV2Service.checkRestoresAsync(); + Restore restore = restoreRepository.findById(restoreName); + assertEquals("newName", restore.getLogicalRestores().getFirst().getRestoreDatabases().getFirst().getUsers().getFirst().getName()); List databaseRegistries = databaseRegistryDbaasRepository.findAnyLogDbRegistryTypeByNamespace(mappedNamespace); assertEquals(4, databaseRegistries.size()); @@ -1515,6 +1516,7 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { assertFalse(database1.isExternallyManageable()); assertFalse(database1.isMarkedForDrop()); assertEquals(newName, database1.getName()); + assertEquals("newName", database1.getConnectionProperties().getFirst().get("username")); // Registry that was copied from DB that was MARKED_FOR_DROP DatabaseRegistry databaseRegistry2 = database1.getDatabaseRegistry().stream() @@ -1564,6 +1566,7 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { assertFalse(database3.isExternallyManageable()); assertTrue(database3.isMarkedForDrop()); assertEquals(newName, database3.getName()); + assertEquals("oldUsername", database3.getConnectionProperties().getFirst().get("username")); DatabaseRegistry databaseRegistry5 = database3.getDatabaseRegistry().stream() .filter(db -> anotherNamespace.equals(db.getNamespace())) @@ -1577,7 +1580,721 @@ void restore_similarRegistryInAnotherNamespace_shouldCreateNewDb() { anotherTenantId.equals(databaseRegistry5.getClassifier().get(TENANT_ID)) && databaseRegistry5.getClassifier().containsKey(MARKED_FOR_DROP) ); + } + + @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(eq(postgresType), anyString(), anyString())) + .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, false + ); + 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()); + + ClassifierDetailsResponse classifier1 = restoreDatabase.getClassifiers().stream() + .filter(classifier -> ClassifierType.REPLACED == classifier.getType()) + .findAny().orElse(null); + assertNotNull(classifier1); + assertNotNull(classifier1.getClassifier()); + assertEquals(database.getName(), classifier1.getPreviousDatabase()); + 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)) + ); + + ClassifierDetailsResponse classifier2 = restoreDatabase.getClassifiers().stream() + .filter(classifier -> ClassifierType.TRANSIENT_REPLACED == classifier.getType()) + .findAny().orElse(null); + assertNotNull(classifier2); + assertNotNull(classifier2.getClassifier()); + assertEquals(database.getName(), classifier2.getPreviousDatabase()); + 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()); + + ClassifierDetailsResponse classifier3 = externalDatabaseResponse.getClassifiers().getFirst(); + assertNotNull(classifier3); + assertEquals(ClassifierType.REPLACED, classifier3.getType()); + assertEquals(externalDb.getName(), classifier3.getPreviousDatabase()); + 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); + + ClassifierDetails classifierWrapper1 = getClassifier(ClassifierType.NEW, mappedNamespace, microserviceName1, null, namespace, null); + ClassifierDetails classifierWrapper2 = getClassifier(ClassifierType.NEW, namespace, microserviceName2, null, namespace, null); + ClassifierDetails classifierWrapper3 = getClassifier(ClassifierType.NEW, namespace, 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 async restore process + LogicalRestoreAdapterResponse response = LogicalRestoreAdapterResponse.builder() + .status(COMPLETED_STATUS) + .restoreId(logicalRestoreName) + .databases(List.of(LogicalRestoreAdapterResponse.RestoreDatabaseResponse.builder() + .status(COMPLETED_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); + + when(dbaasAdapter.trackRestoreV2(eq(logicalRestoreName), any(), any())) + .thenReturn(response); + // Mocks to ensure user process + DbResource resource1 = new DbResource(); + resource1.setId(UUID.randomUUID()); + resource1.setKind("kind"); + resource1.setName("newName"); + EnsuredUser user1 = new EnsuredUser(); + user1.setConnectionProperties(Map.of( + "key", "value" + )); + user1.setResources(List.of(resource1)); + + when(dbaasAdapter.ensureUser(null, null, newName, "admin")).thenReturn(user1); + + DbResource resource2 = new DbResource(); + resource2.setId(UUID.randomUUID()); + resource2.setKind("kind"); + resource2.setName("newName"); + EnsuredUser user2 = new EnsuredUser(); + user2.setConnectionProperties(Map.of( + "key", "value" + )); + user2.setResources(List.of(resource2)); + + when(dbaasAdapter.ensureUser(null, null, newName2, "admin")).thenReturn(user2); + + RestoreResponse restoreResponse = dbBackupV2Service.retryRestore(restoreName, false); + 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(); + assertNull(logicalRestoreResponse.getLogicalRestoreName()); + assertEquals(adapterId, logicalRestoreResponse.getAdapterId()); + assertEquals(2, logicalRestoreResponse.getRestoreDatabases().size()); + + // 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 updateAndValidateClassifier() { + String namespace = "test1-namespace"; + String namespaceMapped = "test1-namespace-mapped"; + + List classifiers = getClassifiers(namespace, namespaceMapped); + Mapping mapping = new Mapping(); + mapping.setNamespaces(Map.of(namespace, namespaceMapped)); + + Set> uniqueClassifiers = new HashSet<>(); + } + + @Test + void updateClassifier_withoutMapping() { + SortedMap oldClassifier = new TreeMap<>(); + oldClassifier.put(NAMESPACE, "namespace"); + oldClassifier.put(MICROSERVICE_NAME, "microserviceName"); + oldClassifier.put(TENANT_ID, "tenant"); + oldClassifier.put(SCOPE, "tenant"); + + ClassifierDetails classifier = new ClassifierDetails(); + classifier.setClassifierBeforeMapper(oldClassifier); + + List updatedClassifiers = dbBackupV2Service.applyMapping(List.of(classifier), null); + assertEquals(1, updatedClassifiers.size()); + + ClassifierDetails updatedClassifier = updatedClassifiers.getFirst(); + assertEquals(updatedClassifier.getClassifier(), updatedClassifier.getClassifierBeforeMapper()); + assertEquals(updatedClassifier.getClassifierBeforeMapper(), oldClassifier); + } + + @Test + void updateClassifier_tenantMapping() { + String tenant = "tenant"; + String mappedTenant = "mappedTenant"; + + SortedMap oldClassifier = new TreeMap<>(); + oldClassifier.put(NAMESPACE, "namespace"); + oldClassifier.put(MICROSERVICE_NAME, "microserviceName"); + oldClassifier.put(TENANT_ID, tenant); + oldClassifier.put(SCOPE, tenant); + + ClassifierDetails classifier = new ClassifierDetails(); + classifier.setClassifierBeforeMapper(oldClassifier); + + Mapping mapping = new Mapping(); + mapping.setTenants(Map.of( + tenant, mappedTenant + )); + + List updatedClassifiers = dbBackupV2Service.applyMapping(List.of(classifier), mapping); + assertEquals(1, updatedClassifiers.size()); + ClassifierDetails updatedClassifier = updatedClassifiers.getFirst(); + SortedMap mappedClassifier = updatedClassifier.getClassifier(); + + assertNotNull(mappedClassifier); + assertEquals(updatedClassifier.getClassifierBeforeMapper(), oldClassifier); + + assertEquals(oldClassifier.get(NAMESPACE), mappedClassifier.get(NAMESPACE)); + assertEquals(oldClassifier.get(MICROSERVICE_NAME), mappedClassifier.get(MICROSERVICE_NAME)); + assertEquals(oldClassifier.get(SCOPE), mappedClassifier.get(SCOPE)); + assertEquals(mappedTenant, mappedClassifier.get(TENANT_ID)); + } + + @Test + void updateClassifier_nullTenantMapping() { + String tenant = "tenant"; + String mappedTenant = "mappedTenant"; + + SortedMap oldClassifier = new TreeMap<>(); + oldClassifier.put(NAMESPACE, "namespace"); + oldClassifier.put(MICROSERVICE_NAME, "microserviceName"); + oldClassifier.put(SCOPE, "service"); + + ClassifierDetails classifier = new ClassifierDetails(); + classifier.setClassifierBeforeMapper(oldClassifier); + + Mapping mapping = new Mapping(); + mapping.setTenants(Map.of( + tenant, mappedTenant + )); + + List updatedClassifiers = dbBackupV2Service.applyMapping(List.of(classifier), mapping); + assertEquals(1, updatedClassifiers.size()); + + ClassifierDetails updatedClassifier = updatedClassifiers.getFirst(); + assertEquals(updatedClassifier.getClassifier(), updatedClassifier.getClassifierBeforeMapper()); + assertEquals(updatedClassifier.getClassifierBeforeMapper(), oldClassifier); + } + + @Test + void updateClassifier_namespaceMapping() { + String namespace = "namespace"; + String mappedNamespace = "mappedNamespace"; + + SortedMap oldClassifier = new TreeMap<>(); + oldClassifier.put(NAMESPACE, namespace); + oldClassifier.put(MICROSERVICE_NAME, "microserviceName"); + oldClassifier.put(SCOPE, "service"); + + ClassifierDetails classifier = new ClassifierDetails(); + classifier.setClassifierBeforeMapper(oldClassifier); + + Mapping mapping = new Mapping(); + mapping.setNamespaces(Map.of( + namespace, mappedNamespace + )); + + List updatedClassifiers = dbBackupV2Service.applyMapping(List.of(classifier), mapping); + assertEquals(1, updatedClassifiers.size()); + + SortedMap mappedClassifier = updatedClassifiers.getFirst().getClassifier(); + + assertEquals(updatedClassifiers.getFirst().getClassifierBeforeMapper(), oldClassifier); + + assertEquals(oldClassifier.get(MICROSERVICE_NAME), mappedClassifier.get(MICROSERVICE_NAME)); + assertEquals(oldClassifier.get(SCOPE), mappedClassifier.get(SCOPE)); + assertEquals(mappedNamespace, mappedClassifier.get(NAMESPACE)); + } + + private static @NotNull List getClassifiers(String namespace, String namespaceMapped) { + SortedMap classifier1 = new TreeMap<>(); + classifier1.put("microserviceName", "test1"); + classifier1.put("namespace", namespace); + classifier1.put("scope", "service"); + + SortedMap classifier2 = new TreeMap<>(); + classifier2.put("microserviceName", "test1"); + classifier2.put("namespace", namespaceMapped); + classifier2.put("scope", "service"); + + ClassifierDetails classifierWrapper1 = new ClassifierDetails(); + classifierWrapper1.setClassifierBeforeMapper(classifier1); + + ClassifierDetails classifierWrapper2 = new ClassifierDetails(); + classifierWrapper2.setClassifierBeforeMapper(classifier2); + + return List.of(classifierWrapper1, classifierWrapper2); + } + + @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.setInclude(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.setInclude(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.setInclude(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.setInclude(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 @@ -1585,12 +2302,316 @@ void getAllDbByFilter_whenDatabasesNotFound() { Filter filter = new Filter(); filter.setNamespace(List.of("namespace")); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); assertThrows(DbNotFoundException.class, () -> 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.setInclude(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()); + + DatabaseWithClassifiers 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.setInclude(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()); + + DatabaseWithClassifiers 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.setInclude(List.of(filter1, filter2)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(2, filteredDatabases.size()); + + DatabaseWithClassifiers 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); + + DatabaseWithClassifiers 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.setInclude(List.of(filter1)); + + List backupDatabases = List.of(backupDatabase1, backupDatabase2, backupDatabase3); + List filteredDatabases = dbBackupV2Service.getAllDbByFilter(backupDatabases, filterCriteria); + + assertEquals(1, filteredDatabases.size()); + + DatabaseWithClassifiers 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.setInclude(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"; @@ -1704,7 +2725,7 @@ void validateAndFilterDatabasesForBackup_whenStrategyFail() { void checkBackupsAsync_shouldNotRunInParallelAcrossNodes() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(2); - when(backupRepository.findBackupsToAggregate()) + when(backupRepository.findBackupsToTrack()) .thenAnswer(new AnswersWithDelay(100, new Returns(List.of()))); Mockito.clearInvocations(backupRepository); @@ -1721,7 +2742,7 @@ void checkBackupsAsync_shouldNotRunInParallelAcrossNodes() throws Exception { executor.shutdown(); executor.awaitTermination(1, SECONDS); - Mockito.verify(backupRepository, Mockito.times(1)).findBackupsToAggregate(); + Mockito.verify(backupRepository, Mockito.times(1)).findBackupsToTrack(); Mockito.reset(backupRepository); } @@ -1782,7 +2803,7 @@ void trackAndAggregate_backupFinishedWithStatusFailed() { .findAny() .ifPresent(db -> db.setStatus(BackupTaskStatus.FAILED)); }); - + Backup updatedBackup = backupRepository.save(backup); DbaasAdapter adapter1 = Mockito.mock(DbaasAdapter.class); when(physicalDatabasesService.getAdapterById("0")) @@ -1790,7 +2811,7 @@ void trackAndAggregate_backupFinishedWithStatusFailed() { when(adapter1.trackBackupV2("logicalBackupName0", "storageName", "blobPath")) .thenReturn(adapterResponse); - dbBackupV2Service.trackAndAggregate(backup); + dbBackupV2Service.trackAndAggregate(updatedBackup); Backup expectedBackup = backupRepository.findById(backupName); assertNotNull(expectedBackup); @@ -1798,7 +2819,7 @@ void trackAndAggregate_backupFinishedWithStatusFailed() { String errorMsg = String.format("LogicalBackup %s failed: %s=Error during backup process", "logicalBackupName1", db3Name); assertEquals(errorMsg, expectedBackup.getErrorMessage()); - assertEquals(1, backup.getAttemptCount()); + assertEquals(1, expectedBackup.getAttemptCount()); } @Test @@ -1825,7 +2846,7 @@ void trackAndAggregate_backupAttemptExceeded_aggregatorMustBeFailed() { @Test void aggregateStatus_shouldReturnInProgress_whenInputInProgressFailedCompleted() { Set statusSet = Set.of(BackupTaskStatus.IN_PROGRESS, BackupTaskStatus.FAILED, BackupTaskStatus.COMPLETED); - BackupStatus backupStatus = dbBackupV2Service.aggregateBackupStatus(statusSet); + BackupStatus backupStatus = dbBackupV2Service.aggregateBackupTaskStatus(statusSet); assertNotNull(backupStatus); assertEquals(BackupStatus.IN_PROGRESS, backupStatus); @@ -1834,7 +2855,7 @@ void aggregateStatus_shouldReturnInProgress_whenInputInProgressFailedCompleted() @Test void aggregateStatus_shouldReturnInProgress_whenInputNotStartedFailedCompleted() { Set statusSet = Set.of(BackupTaskStatus.NOT_STARTED, BackupTaskStatus.FAILED, BackupTaskStatus.COMPLETED); - BackupStatus backupStatus = dbBackupV2Service.aggregateBackupStatus(statusSet); + BackupStatus backupStatus = dbBackupV2Service.aggregateBackupTaskStatus(statusSet); assertNotNull(backupStatus); assertEquals(BackupStatus.IN_PROGRESS, backupStatus); @@ -1843,7 +2864,7 @@ void aggregateStatus_shouldReturnInProgress_whenInputNotStartedFailedCompleted() @Test void aggregateStatus_shouldReturnFailed_whenInputFailedCompleted() { Set statusSet = Set.of(BackupTaskStatus.FAILED, BackupTaskStatus.COMPLETED); - BackupStatus backupStatus = dbBackupV2Service.aggregateBackupStatus(statusSet); + BackupStatus backupStatus = dbBackupV2Service.aggregateBackupTaskStatus(statusSet); assertNotNull(backupStatus); assertEquals(BackupStatus.FAILED, backupStatus); @@ -1852,7 +2873,7 @@ void aggregateStatus_shouldReturnFailed_whenInputFailedCompleted() { @Test void aggregateStatus_shouldReturnCompleted_whenInputCompleted() { Set statusSet = Set.of(BackupTaskStatus.COMPLETED); - BackupStatus backupStatus = dbBackupV2Service.aggregateBackupStatus(statusSet); + BackupStatus backupStatus = dbBackupV2Service.aggregateBackupTaskStatus(statusSet); assertNotNull(backupStatus); assertEquals(BackupStatus.COMPLETED, backupStatus); @@ -1883,8 +2904,8 @@ void getBackup() { FilterCriteria responseFilterCriteria = response.getFilterCriteria(); assertNotNull(responseFilterCriteria); - assertEquals(1, responseFilterCriteria.getFilter().size()); - assertEquals(namespace, responseFilterCriteria.getFilter().getFirst().getNamespace().getFirst()); + assertEquals(1, responseFilterCriteria.getInclude().size()); + assertEquals(namespace, responseFilterCriteria.getInclude().getFirst().getNamespace().getFirst()); List externalDatabases = backup.getExternalDatabases(); assertNull(externalDatabases); @@ -1957,8 +2978,8 @@ void getRestore() { FilterCriteria responseFilterCriteria = response.getFilterCriteria(); assertNotNull(responseFilterCriteria); - assertEquals(1, responseFilterCriteria.getFilter().size()); - assertEquals(namespace, responseFilterCriteria.getFilter().getFirst().getNamespace().getFirst()); + assertEquals(1, responseFilterCriteria.getInclude().size()); + assertEquals(namespace, responseFilterCriteria.getInclude().getFirst().getNamespace().getFirst()); List externalDatabases = restore.getExternalDatabases(); assertNull(externalDatabases); @@ -2143,6 +3164,7 @@ void uploadBackupMetadata_restoreDeletedBackup_digestMismatch() { BackupResponse backupResponse = getBackupResponse(backupName, namespace); backupResponse.setDigest(anotherDigest); + IntegrityViolationException ex = assertThrows(IntegrityViolationException.class, () -> dbBackupV2Service.uploadBackupMetadata(backupResponse)); assertEquals( @@ -2166,7 +3188,7 @@ void uploadBackupMetadata_restoreDeletedBackup_backupNotImported() { () -> dbBackupV2Service.uploadBackupMetadata(backupResponse)); assertEquals( - String.format("Resource has illegal state: can`t restore %s backup that not imported", + String.format("Resource has illegal state: can't restore a %s backup that is not imported", BackupStatus.DELETED), ex.getDetail()); } @@ -2223,6 +3245,24 @@ void deleteRestore_whenRestoreStatusUnprocessable() { () -> dbBackupV2Service.deleteRestore(restoreName)); } + @Test + void deleteBackupFromDb() { + String backupName = "backupName"; + String namespace = "namespace"; + Backup backup = getBackup(backupName, namespace); + backupRepository.save(backup); + + Backup existedBackup = backupRepository.findById(backupName); + assertNotNull(existedBackup); + + when(dbaaSHelper.isProductionMode()).thenReturn(false); + + dbBackupV2Service.deleteBackupFromDb(backupName); + + Backup deletedBackup = backupRepository.findById(backupName); + assertNull(deletedBackup); + } + private Database getDatabase(String adapterId, String name, boolean isExternal, boolean isBackupDisabled, String bgVersion) { DbState dbState = new DbState(); dbState.setId(UUID.randomUUID()); @@ -2233,7 +3273,7 @@ private Database getDatabase(String adapterId, String name, boolean isExternal, Map map = new HashMap<>(); map.put("role", "admin"); - map.put("username", "username"); + map.put("username", "oldUsername"); Database database = new Database(); database.setId(UUID.randomUUID()); database.setAdapterId(adapterId); @@ -2275,12 +3315,44 @@ private SortedMap getClassifier(String namespace, String microse return classifier; } + private ClassifierDetails getClassifier(ClassifierType classifierType, String namespace, String microserviceName, String tenantId, String namespaceBeforeMap, String tenantIdBeforeMap) { + assertFalse(namespaceBeforeMap.isBlank()); + + ClassifierDetails classifierWrapper = new ClassifierDetails(); + 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) - .type(type) - .classifiers(classifiers) - .build(); + BackupExternalDatabase externalDatabase = new BackupExternalDatabase(); + externalDatabase.setId(UUID.randomUUID()); + externalDatabase.setName(name); + externalDatabase.setType(type); + externalDatabase.setClassifiers(classifiers); + return externalDatabase; } private BackupDatabase getBackupDatabase(String dbName, @@ -2288,18 +3360,19 @@ private BackupDatabase getBackupDatabase(String dbName, boolean configurational, BackupTaskStatus status, String errorMessage) { - return BackupDatabase.builder() - .name(dbName) - .classifiers(classifiers) - .settings(Map.of("setting", "setting")) - .users(List.of(new BackupDatabase.User("username", "admin"))) - .configurational(configurational) - .status(status) - .size(1) - .duration(1) - .path("path") - .errorMessage(errorMessage) - .build(); + BackupDatabase backupDatabase = new BackupDatabase(); + backupDatabase.setId(UUID.randomUUID()); + backupDatabase.setName(dbName); + backupDatabase.setClassifiers(classifiers); + backupDatabase.setSettings(Map.of("setting", "setting")); + backupDatabase.setUsers(List.of(new BackupDatabase.User("oldUsername", "admin"))); + backupDatabase.setConfigurational(configurational); + backupDatabase.setStatus(status); + backupDatabase.setSize(1); + backupDatabase.setDuration(1); + backupDatabase.setPath("path"); + backupDatabase.setErrorMessage(errorMessage); + return backupDatabase; } private LogicalBackup getLogicalBackup(String logicalBackupName, @@ -2309,14 +3382,14 @@ private LogicalBackup getLogicalBackup(String logicalBackupName, BackupTaskStatus status, String errorMsg ) { - LogicalBackup logicalBackup = LogicalBackup.builder() - .logicalBackupName(logicalBackupName) - .adapterId(adapterId) - .type(type) - .backupDatabases(backupDatabases) - .status(status) - .errorMessage(errorMsg) - .build(); + LogicalBackup logicalBackup = new LogicalBackup(); + logicalBackup.setId(UUID.randomUUID()); + logicalBackup.setLogicalBackupName(logicalBackupName); + logicalBackup.setAdapterId(adapterId); + logicalBackup.setType(type); + logicalBackup.setBackupDatabases(backupDatabases); + logicalBackup.setStatus(status); + logicalBackup.setErrorMessage(errorMsg); backupDatabases.forEach(db -> db.setLogicalBackup(logicalBackup)); return logicalBackup; @@ -2330,26 +3403,25 @@ private Backup getBackup(String name, BackupStatus status, String errorMsg ) { - Backup backup = Backup.builder() - .name(name) - .storageName(STORAGE_NAME) - .blobPath(BLOB_PATH) - .externalDatabaseStrategy(strategy) - .filterCriteria(filterCriteria) - .logicalBackups(logicalBackups) - .externalDatabases(externalDatabases) - .status(status) - .total(logicalBackups.stream().mapToInt(db -> db.getBackupDatabases().size()).sum()) - .completed((int) logicalBackups.stream() - .flatMap(db -> db.getBackupDatabases().stream()) - .filter(bd -> BackupTaskStatus.COMPLETED == bd.getStatus()) - .count()) - .size(logicalBackups.stream() - .flatMap(db -> db.getBackupDatabases().stream()) - .mapToLong(BackupDatabase::getSize) - .sum()) - .errorMessage(errorMsg) - .build(); + Backup backup = new Backup(); + backup.setName(name); + backup.setStorageName(STORAGE_NAME); + backup.setBlobPath(BLOB_PATH); + backup.setExternalDatabaseStrategy(strategy); + backup.setFilterCriteria(filterCriteria); + backup.setLogicalBackups(logicalBackups); + backup.setExternalDatabases(externalDatabases); + backup.setStatus(status); + backup.setTotal(logicalBackups.stream().mapToInt(db -> db.getBackupDatabases().size()).sum()); + backup.setCompleted((int) logicalBackups.stream() + .flatMap(db -> db.getBackupDatabases().stream()) + .filter(bd -> BackupTaskStatus.COMPLETED == bd.getStatus()) + .count()); + backup.setSize(logicalBackups.stream() + .flatMap(db -> db.getBackupDatabases().stream()) + .mapToLong(BackupDatabase::getSize) + .sum()); + backup.setErrorMessage(errorMsg); logicalBackups.forEach(db -> db.setBackup(backup)); externalDatabases.forEach(db -> db.setBackup(backup)); @@ -2362,27 +3434,24 @@ private Backup getBackup(String backupName, String namespace) { List backupDatabases = new ArrayList<>(); for (int i = 0; i < 3; i++) { - BackupDatabase backupDatabase = BackupDatabase.builder() - .name("db" + i) - .users(List.of( - BackupDatabase.User.builder() - .name("username") - .role("role") - .build() - )) - .path("path") - .classifiers(List.of(classifier)) - .build(); + BackupDatabase backupDatabase = new BackupDatabase(); + backupDatabase.setId(UUID.randomUUID()); + backupDatabase.setName("db" + i); + backupDatabase.setUsers(List.of(new BackupDatabase.User("oldUsername", "role"))); + backupDatabase.setStatus(BackupTaskStatus.COMPLETED); + backupDatabase.setPath("path"); + backupDatabase.setClassifiers(List.of(classifier)); backupDatabases.add(backupDatabase); } List logicalBackups = new ArrayList<>(); for (int i = 0; i < 2; i++) { - LogicalBackup logicalBackup = LogicalBackup.builder() - .logicalBackupName("logicalBackupName" + i) - .adapterId(Integer.toString(i)) - .type("postgresql") - .build(); + LogicalBackup logicalBackup = new LogicalBackup(); + logicalBackup.setId(UUID.randomUUID()); + logicalBackup.setLogicalBackupName("logicalBackupName" + i); + logicalBackup.setStatus(BackupTaskStatus.COMPLETED); + logicalBackup.setAdapterId(Integer.toString(i)); + logicalBackup.setType("postgresql"); logicalBackups.add(logicalBackup); } @@ -2397,12 +3466,11 @@ private Backup getBackup(String backupName, String namespace) { secondLogical.setBackupDatabases(second); second.forEach(db -> db.setLogicalBackup(secondLogical)); - FilterEntity filter = FilterEntity.builder() - .namespace(List.of(namespace)) - .build(); - FilterCriteriaEntity criteriaEntity = FilterCriteriaEntity.builder() - .filter(List.of(filter)) - .build(); + FilterEntity filter = new FilterEntity(); + filter.setNamespace(List.of(namespace)); + + FilterCriteriaEntity criteriaEntity = new FilterCriteriaEntity(); + criteriaEntity.setInclude(List.of(filter)); Backup backup = new Backup(); backup.setName(backupName); @@ -2420,26 +3488,29 @@ private Restore getRestore(String restoreName, String namespace) { SortedMap classifier = new TreeMap<>(); classifier.put("namespace", namespace); + ClassifierDetails classifierMapper = new ClassifierDetails(); + classifierMapper.setClassifierBeforeMapper(classifier); + List restoreDatabases = new ArrayList<>(); for (int i = 0; i < 3; i++) { - RestoreDatabase restoreDatabase = RestoreDatabase.builder() - .name("db" + i) - .users(List.of( - new RestoreDatabase.User("username", "admin") - )) - .path("path") - .classifiers(List.of(classifier)) - .build(); + RestoreDatabase restoreDatabase = new RestoreDatabase(); + restoreDatabase.setId(UUID.randomUUID()); + restoreDatabase.setName("db" + i); + restoreDatabase.setUsers(List.of(new RestoreDatabase.User("oldUsername", "admin"))); + restoreDatabase.setStatus(RestoreTaskStatus.COMPLETED); + restoreDatabase.setPath("path"); + restoreDatabase.setClassifiers(List.of(classifierMapper)); restoreDatabases.add(restoreDatabase); } List logicalRestores = new ArrayList<>(); for (int i = 0; i < 2; i++) { - LogicalRestore logicalRestore = LogicalRestore.builder() - .logicalRestoreName("logicalRestoreName" + i) - .adapterId(Integer.toString(i)) - .type("postgresql") - .build(); + LogicalRestore logicalRestore = new LogicalRestore(); + logicalRestore.setId(UUID.randomUUID()); + logicalRestore.setLogicalRestoreName("logicalRestoreName" + i); + logicalRestore.setStatus(RestoreTaskStatus.COMPLETED); + logicalRestore.setAdapterId(Integer.toString(i)); + logicalRestore.setType("postgresql"); logicalRestores.add(logicalRestore); } @@ -2454,12 +3525,10 @@ private Restore getRestore(String restoreName, String namespace) { secondLogical.setRestoreDatabases(second); second.forEach(db -> db.setLogicalRestore(secondLogical)); - FilterEntity filter = FilterEntity.builder() - .namespace(List.of(namespace)) - .build(); - FilterCriteriaEntity criteriaEntity = FilterCriteriaEntity.builder() - .filter(List.of(filter)) - .build(); + FilterEntity filter = new FilterEntity(); + filter.setNamespace(List.of(namespace)); + FilterCriteriaEntity criteriaEntity = new FilterCriteriaEntity(); + criteriaEntity.setInclude(List.of(filter)); Restore restore = new Restore(); restore.setName(restoreName); @@ -2510,23 +3579,24 @@ private Map> getDatabase(Map db private RestoreDatabase getRestoreDatabase(BackupDatabase backupDatabase, String dbName, - List> classifiers, + List classifiers, Map settings, String bgVersion, RestoreTaskStatus status, long duration, String errorMessage) { - return RestoreDatabase.builder() - .backupDatabase(backupDatabase) - .name(dbName) - .classifiers(classifiers) - .settings(settings) - .users(List.of(new RestoreDatabase.User("username", "admin"))) - .bgVersion(bgVersion) - .status(status) - .duration(duration) - .errorMessage(errorMessage) - .build(); + RestoreDatabase db = new RestoreDatabase(); + db.setId(UUID.randomUUID()); + db.setBackupDatabase(backupDatabase); + db.setName(dbName); + db.setClassifiers(classifiers); + db.setSettings(settings); + db.setUsers(List.of(new RestoreDatabase.User("oldUsername", "admin"))); + db.setBgVersion(bgVersion); + db.setStatus(status); + db.setDuration(duration); + db.setErrorMessage(errorMessage); + return db; } private LogicalRestore getLogicalRestore(String logicalRestoreName, @@ -2535,14 +3605,14 @@ private LogicalRestore getLogicalRestore(String logicalRestoreName, List restoreDatabases, RestoreTaskStatus status, String errorMsg) { - LogicalRestore logicalRestore = LogicalRestore.builder() - .logicalRestoreName(logicalRestoreName) - .adapterId(adapterId) - .type(type) - .restoreDatabases(restoreDatabases) - .status(status) - .errorMessage(errorMsg) - .build(); + LogicalRestore logicalRestore = new LogicalRestore(); + logicalRestore.setId(UUID.randomUUID()); + logicalRestore.setLogicalRestoreName(logicalRestoreName); + logicalRestore.setAdapterId(adapterId); + logicalRestore.setType(type); + logicalRestore.setRestoreDatabases(restoreDatabases); + logicalRestore.setStatus(status); + logicalRestore.setErrorMessage(errorMsg); restoreDatabases.forEach(db -> db.setLogicalRestore(logicalRestore)); return logicalRestore; @@ -2557,25 +3627,38 @@ private Restore getRestore(Backup backup, List externalDatabases, RestoreStatus status, String errorMsg) { - Restore restore = Restore.builder() - .name(name) - .backup(backup) - .storageName(STORAGE_NAME) - .blobPath(BLOB_PATH) - .filterCriteria(filterCriteria) - .mapping(mapping) - .logicalRestores(logicalRestores) - .externalDatabaseStrategy(strategy) - .externalDatabases(externalDatabases) - .status(status) - .errorMessage(errorMsg) - .build(); + + Restore restore = new Restore(); + restore.setName(name); + restore.setBackup(backup); + restore.setStorageName(STORAGE_NAME); + restore.setBlobPath(BLOB_PATH); + restore.setFilterCriteria(filterCriteria); + restore.setMapping(mapping); + restore.setLogicalRestores(logicalRestores); + restore.setExternalDatabaseStrategy(strategy); + restore.setExternalDatabases(externalDatabases); + restore.setStatus(status); + restore.setErrorMessage(errorMsg); logicalRestores.forEach(db -> db.setRestore(restore)); externalDatabases.forEach(db -> db.setRestore(restore)); return restore; } + private RestoreExternalDatabase getRestoreExternalDb( + String name, + String type, + List classifiers + ) { + RestoreExternalDatabase db = new RestoreExternalDatabase(); + db.setId(UUID.randomUUID()); + db.setName(name); + db.setType(type); + db.setClassifiers(classifiers); + return db; + } + private BackupRequest getBackupRequest(String backupName, List namespaces, ExternalDatabaseStrategy strategy, @@ -2585,7 +3668,7 @@ private BackupRequest getBackupRequest(String backupName, filter.setNamespace(namespaces); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); BackupRequest dto = new BackupRequest(); dto.setFilterCriteria(filterCriteria); @@ -2608,7 +3691,7 @@ private RestoreRequest getRestoreRequest( filter.setNamespace(namespaces); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); Mapping mapping = new Mapping(); mapping.setNamespaces(namespaceMapping); @@ -2630,6 +3713,7 @@ private BackupResponse getBackupResponse(String backupName, String namespace) { sortedMap.put("key-second", Map.of("inner-key", "inner-value")); BackupDatabaseResponse backupDatabaseResponse = new BackupDatabaseResponse( + UUID.randomUUID(), "backup-database", List.of(sortedMap), Map.of("settings-key", "settings-value"), @@ -2647,6 +3731,7 @@ private BackupResponse getBackupResponse(String backupName, String namespace) { ); LogicalBackupResponse logicalBackupResponse = new LogicalBackupResponse( + UUID.randomUUID(), "logicalBackupName", "adapterID", "type", @@ -2661,12 +3746,13 @@ private BackupResponse getBackupResponse(String backupName, String namespace) { filter.setNamespace(List.of(namespace)); FilterCriteria filterCriteria = new FilterCriteria(); - filterCriteria.setFilter(List.of(filter)); + filterCriteria.setInclude(List.of(filter)); SortedMap map = new TreeMap<>(); map.put("key", "value"); BackupExternalDatabaseResponse backupExternalDatabase = new BackupExternalDatabaseResponse(); + backupExternalDatabase.setId(UUID.randomUUID()); backupExternalDatabase.setName("Name"); backupExternalDatabase.setType("postgresql"); backupExternalDatabase.setClassifiers(List.of(map)); @@ -2690,12 +3776,11 @@ private BackupResponse getBackupResponse(String backupName, String namespace) { } private FilterCriteriaEntity getFilterCriteriaEntity(List namespaces) { - FilterEntity filter = FilterEntity.builder() - .namespace(namespaces) - .build(); + FilterEntity filter = new FilterEntity(); + filter.setNamespace(namespaces); - return FilterCriteriaEntity.builder() - .filter(List.of(filter)) - .build(); + FilterCriteriaEntity filterCriteria = new FilterCriteriaEntity(); + filterCriteria.setInclude(List.of(filter)); + return filterCriteria; } } diff --git a/docs/OpenAPI.json b/docs/OpenAPI.json index f45181c5..973f620d 100644 --- a/docs/OpenAPI.json +++ b/docs/OpenAPI.json @@ -140,12 +140,21 @@ "BackupDatabaseResponse": { "type": "object", "required": [ + "id", "name", "status", "path" ], "description": "Logical database backup details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the backup database" + }, "name": { "type": "string", "examples": [ @@ -173,7 +182,9 @@ "settings": { "type": "object", "examples": [ - "{\"key\":value, \"key\":value}" + { + "key": "value" + } ], "additionalProperties": {}, "description": "Database settings as a key-value map" @@ -181,7 +192,12 @@ "users": { "type": "array", "examples": [ - "[{\"name\":\"username\",\"role\":\"admin\"}" + [ + { + "name": "username", + "role": "admin" + } + ] ], "items": { "$ref": "#/components/schemas/User" @@ -204,6 +220,7 @@ "NOT_STARTED", "IN_PROGRESS", "FAILED", + "RETRYABLE_FAIL", "COMPLETED" ], "description": "Current state of the backup database" @@ -251,10 +268,19 @@ "BackupExternalDatabaseResponse": { "type": "object", "required": [ + "id", "name" ], "description": "External database details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the external backup database" + }, "name": { "type": "string", "examples": [ @@ -306,7 +332,7 @@ "before-prod-update-20251013T1345-G5s8" ], "pattern": "\\S", - "description": "Unique identifier of the backup" + "description": "Unique name of the backup" }, "storageName": { "type": "string", @@ -326,18 +352,8 @@ }, "filterCriteria": { "type": "object", - "required": [ - "filter" - ], "description": "Filter criteria", "properties": { - "filter": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Apply the filter to the remaining databases" - }, "include": { "type": "array", "items": { @@ -388,7 +404,10 @@ "blobPath", "externalDatabaseStrategy", "ignoreNotBackupableDatabases", - "status" + "status", + "total", + "completed", + "size" ], "description": "Response containing backup operation details", "properties": { @@ -398,7 +417,7 @@ "before-prod-update-20251013T1345-G5s8" ], "pattern": "\\S", - "description": "Unique identifier of the backup" + "description": "Unique name of the backup" }, "storageName": { "type": "string", @@ -436,22 +455,12 @@ "examples": [ false ], - "description": "Whether external databases were skipped during the backup" + "description": "Whether non‑backupable databases were ignored during backup" }, "filterCriteria": { "type": "object", - "required": [ - "filter" - ], "description": "Filter criteria", "properties": { - "filter": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Apply the filter to the remaining databases" - }, "include": { "type": "array", "items": { @@ -654,6 +663,65 @@ } } }, + "ClassifierDetailsResponse": { + "type": "object", + "required": [ + "type", + "classifier" + ], + "description": "Classifier details used during restore operation", + "properties": { + "type": { + "type": [ + "string", + "object" + ], + "enum": [ + "NEW", + "REPLACED", + "TRANSIENT_REPLACED" + ], + "description": "Type of classifier in restore context" + }, + "previousDatabase": { + "type": [ + "string", + "null" + ], + "examples": [ + "dbaas_12345" + ], + "description": "Name of the existing database previously associated with this classifier, used when the classifier replaces or transiently replaces another database during restore" + }, + "classifier": { + "type": "object", + "examples": [ + { + "namespace": "namespace", + "microserviceName": "microserviceName", + "scope": "service" + } + ], + "additionalProperties": {}, + "description": "Final classifier used to create a database in the target environment." + }, + "classifierBeforeMapper": { + "type": [ + "object", + "null" + ], + "examples": [ + { + "namespace": "namespace", + "microserviceName": "microserviceName", + "scope": "service" + } + ], + "additionalProperties": {}, + "description": "Original (pre-mapping) classifier from backup database preserved to track how mapping changed the classifier during restore" + } + } + }, "ClassifierWithRolesRequest": { "type": "object", "required": [ @@ -1790,18 +1858,8 @@ }, "FilterCriteria": { "type": "object", - "required": [ - "filter" - ], "description": "Filter criteria", "properties": { - "filter": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Apply the filter to the remaining databases" - }, "include": { "type": "array", "items": { @@ -1982,6 +2040,7 @@ "LogicalBackupResponse": { "type": "object", "required": [ + "id", "logicalBackupName", "adapterId", "type", @@ -1990,6 +2049,14 @@ ], "description": "Logical backup details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the logical backup" + }, "logicalBackupName": { "type": "string", "description": "Name of the logical backup in adapter" @@ -2014,6 +2081,7 @@ "NOT_STARTED", "IN_PROGRESS", "FAILED", + "RETRYABLE_FAIL", "COMPLETED" ], "description": "Current state of the backup databases of one adapter" @@ -2053,6 +2121,7 @@ "LogicalRestoreResponse": { "type": "object", "required": [ + "id", "logicalRestoreName", "adapterId", "type", @@ -2061,6 +2130,14 @@ ], "description": "Logical restore details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the logical restore" + }, "logicalRestoreName": { "type": "string", "description": "Name of the logical restore in adapter" @@ -2095,6 +2172,7 @@ "NOT_STARTED", "IN_PROGRESS", "FAILED", + "RETRYABLE_FAIL", "COMPLETED" ], "description": "Current state of the restore operation" @@ -3074,12 +3152,21 @@ "RestoreDatabaseResponse": { "type": "object", "required": [ + "id", "name", "status", "path" ], "description": "Logical database restore details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the restore database" + }, "name": { "type": "string", "examples": [ @@ -3099,8 +3186,7 @@ ] ], "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/ClassifierDetailsResponse" }, "description": "List of database classifiers. Each classifier is a sorted map of attributes." }, @@ -3117,7 +3203,9 @@ "settings": { "type": "object", "examples": [ - "{\"key\":value, \"key\":value}" + { + "key": "value" + } ], "additionalProperties": {}, "description": "Database settings as a key-value map" @@ -3138,6 +3226,7 @@ "NOT_STARTED", "IN_PROGRESS", "FAILED", + "RETRYABLE_FAIL", "COMPLETED" ], "description": "Current state of the restore database" @@ -3162,7 +3251,7 @@ "examples": [ "Restore Not Found" ], - "description": "Error message if the backup failed" + "description": "Error message if the restore failed" }, "creationTime": { "$ref": "#/components/schemas/Instant", @@ -3177,10 +3266,19 @@ "RestoreExternalDatabaseResponse": { "type": "object", "required": [ + "id", "name" ], "description": "External database details", "properties": { + "id": { + "$ref": "#/components/schemas/UUID", + "type": "string", + "examples": [ + "550e8400-e29b-41d4-a716-446655440000" + ], + "description": "Identifier of the external restore database" + }, "name": { "type": "string", "examples": [ @@ -3197,20 +3295,10 @@ }, "classifiers": { "type": "array", - "examples": [ - [ - { - "namespace": "namespace", - "microserviceName": "microserviceName", - "scope": "service" - } - ] - ], "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/ClassifierDetailsResponse" }, - "description": "List of database classifiers. Each classifier is a sorted map of attributes." + "description": "List of classifier objects describing database attributes." } } }, @@ -3252,13 +3340,15 @@ "examples": [ "restore-before-prod-update-20251203T1020-4t6S" ], - "description": "Unique identifier of the restore" + "pattern": "\\S", + "description": "Unique name of the restore" }, "storageName": { "type": "string", "examples": [ "s3-backend" ], + "pattern": "\\S", "description": "Name of the storage backend containing the restore" }, "blobPath": { @@ -3266,22 +3356,13 @@ "examples": [ "/backups" ], + "pattern": "\\S", "description": "Path to the restore file in the storage" }, "filterCriteria": { "type": "object", - "required": [ - "filter" - ], "description": "Filter criteria", "properties": { - "filter": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Apply the filter to the remaining databases" - }, "include": { "type": "array", "items": { @@ -3350,8 +3431,12 @@ "type": "object", "required": [ "restoreName", + "backupName", + "storageName", "externalDatabaseStrategy", - "status" + "status", + "total", + "completed" ], "description": "Response containing the restore operation details", "properties": { @@ -3360,20 +3445,23 @@ "examples": [ "restore-before-prod-update-20251203T1020-4t6S" ], - "description": "Unique identifier of the restore" + "pattern": "\\S", + "description": "Unique name of the restore" }, "backupName": { "type": "string", "examples": [ "before-prod-update-20251013T1345-G5s8" ], - "description": "Unique identifier of the backup" + "pattern": "\\S", + "description": "Unique name of the backup" }, "storageName": { "type": "string", "examples": [ "s3-backend" ], + "pattern": "\\S", "description": "Name of the storage backend containing the restore" }, "blobPath": { @@ -3400,18 +3488,8 @@ }, "filterCriteria": { "type": "object", - "required": [ - "filter" - ], "description": "Criteria used to filter restore operations", "properties": { - "filter": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Filter" - }, - "description": "Apply the filter to the remaining databases" - }, "include": { "type": "array", "items": { @@ -3495,22 +3573,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": { @@ -4224,7 +4286,7 @@ "/api/backups/v1/backup": { "post": { "summary": "Initiate database backup", - "description": "Starts an asynchronous backup operation for the specified databases. Returns immediately with a backup identifier that can be used to track progress.", + "description": "Starts an asynchronous backup operation for the specified databases. Returns immediately with a backup name that can be used to track progress.", "tags": [ "Backup \u0026 Restore" ], @@ -4240,14 +4302,14 @@ ], "requestBody": { "description": "Backup request", - "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BackupRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -4307,7 +4369,7 @@ } }, "422": { - "description": "The request was accepted, but the server could`t process due to incompatible resource", + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", "content": { "application/json": { "schema": { @@ -4325,16 +4387,6 @@ } } } - }, - "501": { - "description": "The server does not support the functionality required to fulfill the request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TmfErrorResponse" - } - } - } } }, "security": [ @@ -4355,12 +4407,13 @@ ], "parameters": [ { - "description": "Unique identifier of the backup", + "description": "Unique name of the backup", "required": true, "name": "backupName", "in": "path", "schema": { - "type": "string" + "type": "string", + "pattern": "\\S" } } ], @@ -4375,6 +4428,16 @@ } } }, + "400": { + "description": "The request was invalid or cannot be served", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "401": { "description": "Authentication is required and has failed or has not been provided" }, @@ -4418,12 +4481,13 @@ ], "parameters": [ { - "description": "Unique identifier of the backup", + "description": "Unique name of the backup", "required": true, "name": "backupName", "in": "path", "schema": { - "type": "string" + "type": "string", + "pattern": "\\S" } }, { @@ -4442,14 +4506,24 @@ "204": { "description": "Backup deleted successfully" }, + "400": { + "description": "The request was invalid or cannot be served", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "401": { "description": "Authentication is required and has failed or has not been provided" }, "403": { "description": "The request was valid, but the server is refusing action" }, - "404": { - "description": "The requested resource could not be found", + "422": { + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", "content": { "application/json": { "schema": { @@ -4487,7 +4561,7 @@ ], "parameters": [ { - "description": "Unique identifier of the backup", + "description": "Unique name of the backup", "required": true, "name": "backupName", "in": "path", @@ -4519,6 +4593,16 @@ } } }, + "400": { + "description": "The request was invalid or cannot be served", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "401": { "description": "Authentication is required and has failed or has not been provided" }, @@ -4536,7 +4620,7 @@ } }, "422": { - "description": "The request was accepted, but the server could`t process due to incompatible resource", + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", "content": { "application/json": { "schema": { @@ -4568,13 +4652,13 @@ "/api/backups/v1/backup/{backupName}/restore": { "post": { "summary": "Restore from backup", - "description": "Initiate a database restore operation from an existing backup.This operation is asynchronous and returns immediately with a restore identifier that can be used to track progress.Operation is not idempotent", + "description": "Initiate a database restore operation from an existing backup.This operation is asynchronous and returns immediately with a restore name that can be used to track progress.Operation is not idempotent", "tags": [ "Backup \u0026 Restore" ], "parameters": [ { - "description": "Unique identifier of the backup", + "description": "Unique name of the backup", "required": true, "name": "backupName", "in": "path", @@ -4594,14 +4678,14 @@ ], "requestBody": { "description": "Restore request", - "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RestoreRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -4661,7 +4745,7 @@ } }, "422": { - "description": "The request was accepted, but the server could`t process due to incompatible resource", + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", "content": { "application/json": { "schema": { @@ -4679,16 +4763,6 @@ } } } - }, - "501": { - "description": "The server does not support the functionality required to fulfill the request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TmfErrorResponse" - } - } - } } }, "security": [ @@ -4709,7 +4783,7 @@ ], "parameters": [ { - "description": "Unique identifier of the backup", + "description": "Unique name of the backup", "required": true, "name": "backupName", "in": "path", @@ -4730,6 +4804,16 @@ } } }, + "400": { + "description": "The request was invalid or cannot be served", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "401": { "description": "Authentication is required and has failed or has not been provided" }, @@ -4769,34 +4853,34 @@ "/api/backups/v1/operation/uploadMetadata": { "post": { "summary": "Upload backup metadata", - "description": "Metadata upload done", + "description": "Upload backup metadata", "tags": [ "Backup \u0026 Restore" ], "parameters": [ { - "description": "Digest header in format: sha-256\u003d\u003cbase64-hash\u003e", + "description": "Digest header in format: SHA-256\u003d\u003cbase64-hash\u003e", "in": "header", "name": "Digest", "required": true, "schema": { "type": "string", "examples": [ - "sha-256\u003dnOJRJg..." + "SHA-256\u003dnOJRJg..." ] } } ], "requestBody": { "description": "Backup metadata", - "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BackupResponse" } } - } + }, + "required": true }, "responses": { "200": { @@ -4857,7 +4941,7 @@ ], "parameters": [ { - "description": "Unique identifier of the restore operation", + "description": "Unique name of the restore operation", "required": true, "name": "restoreName", "in": "path", @@ -4878,6 +4962,16 @@ } } }, + "400": { + "description": "The request was invalid or cannot be served", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "401": { "description": "Authentication is required and has failed or has not been provided" }, @@ -4921,12 +5015,13 @@ ], "parameters": [ { - "description": "Unique identifier of the restore operation", + "description": "Unique name of the restore operation", "required": true, "name": "restoreName", "in": "path", "schema": { - "type": "string" + "type": "string", + "pattern": "\\S" } } ], @@ -4940,8 +5035,8 @@ "403": { "description": "The request was valid, but the server is refusing action" }, - "404": { - "description": "The requested resource could not be found", + "422": { + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", "content": { "application/json": { "schema": { @@ -4979,26 +5074,17 @@ ], "parameters": [ { - "description": "Unique identifier of the restore operation", + "description": "Unique name of the restore operation", "required": true, "name": "restoreName", "in": "path", "schema": { - "type": "string" + "type": "string", + "pattern": "\\S" } } ], "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": { @@ -5025,6 +5111,26 @@ } } }, + "409": { + "description": "The request could not be completed due to a conflict with the current state of the resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, + "422": { + "description": "The request was accepted, but the server couldn\u0027t process due to incompatible resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TmfErrorResponse" + } + } + } + }, "500": { "description": "An unexpected error occurred on the server", "content": { @@ -5054,7 +5160,7 @@ ], "parameters": [ { - "description": "Unique identifier of the restore operation", + "description": "Unique name of the restore operation", "required": true, "name": "restoreName", "in": "path",