Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
07e49bd
feat: expand filtering
Logaka Dec 2, 2025
9765ff6
refactor: rewrite dynamic SQL filter
Logaka Dec 8, 2025
e0963a5
Merge branch 'main' into feat/backupV2-add-complex-filtering
Logaka Dec 8, 2025
f665ab3
feat: add NotEmptyFilter validation to Filter class
Logaka Dec 9, 2025
af1004a
feat: expanded filtering for external databases
Logaka Dec 9, 2025
5563e44
feat: add dryRun
Logaka Dec 15, 2025
b011b17
Merge branch 'refs/heads/main' into feat/restore-dryRun
Logaka Dec 15, 2025
31e33e7
feat: change restore classifier variable
Logaka Dec 18, 2025
fb32b57
Merge branch 'main' into feat/backupV2-add-complex-filtering
Logaka Dec 18, 2025
6b56a7d
fix: prevent empty filter from generating invalid WHERE clause
Logaka Dec 18, 2025
891c12f
fix: prevent state-changing logic during dry-run in findSimilarDbByCl…
Logaka Dec 19, 2025
9ba3487
test: add tests for dry-run
Logaka Dec 22, 2025
7380508
refactor: reorganize packages
Logaka Dec 22, 2025
d279bd2
refactor: remove attemptCount, duration fields
Logaka Dec 22, 2025
987e061
Merge branch 'main' into feat/retry-restore
Logaka Dec 23, 2025
398e385
feat: implement retry restore
Logaka Dec 25, 2025
ed61408
refactor: conditional return to stream map
Logaka Dec 25, 2025
a20d0e6
Merge branch 'feat/restore-dryRun' into feat/retry-restore
Logaka Dec 25, 2025
ea34805
refactor: prevent lock method duplication
Logaka Dec 26, 2025
e912a3f
refactor: add validation for RestoreRequest
Logaka Jan 16, 2026
8ad5d86
refactor: make databaseKind join conditional
Logaka Jan 19, 2026
9f6d293
refactor: make kinds validation flexible
Logaka Jan 19, 2026
371fb8a
feat: add new attribute to Classifier entity
Logaka Jan 20, 2026
f405a8e
refactor: change restore processing exceptions
Logaka Jan 20, 2026
8034dbe
refactor: rewrite collision catching logic
Logaka Jan 20, 2026
2ec4e9d
Merge branch 'feat/backupV2-add-complex-filtering' into feat/restore-…
Logaka Jan 21, 2026
a0bf7cd
Merge branch 'feat/retry-restore' into feat/restore-dryRun
Logaka Jan 21, 2026
8856251
chore: reformat code
Logaka Jan 21, 2026
1d55eac
feat: add validation annotations for dto
Logaka Jan 22, 2026
76cdd39
chore: rename method, logs, error messages
Logaka Jan 22, 2026
b108e69
refactor: simplify mapping by removing BackupExternalDelegate
Logaka Jan 22, 2026
5d63dca
feat: prevent restore empty databases
Logaka Jan 26, 2026
2194470
Merge branch 'main' into feat/restore-dryRun
Logaka Jan 26, 2026
5862147
fix: adding missed statuses for OpenApi
Logaka Jan 28, 2026
b85d622
refactor: separate mapping, enriching classifiers operation
Logaka Jan 28, 2026
b3fbe74
fix: remove NOT_STARTED status from aggregate method and fix mapping
Logaka Jan 29, 2026
b56a238
style: remove redundant required attribute
Logaka Jan 29, 2026
98874ae
style: fix OpenApi spec
Logaka Jan 30, 2026
1260506
feat: add new flyway migration that mark all status fields in entitie…
Logaka Jan 30, 2026
09d947b
fix: add default status to logical backup
Logaka Jan 30, 2026
d4a010f
fix: take out try catch block from transactional method
Logaka Jan 30, 2026
76838e3
chore: remove unused imports
Logaka Jan 30, 2026
ed2b81e
refactor: invert methods
Logaka Jan 30, 2026
ec959f2
refactor: replace entity builder with constructor
Logaka Jan 30, 2026
d313d81
refactor: replace entity builder with constructor
Logaka Feb 2, 2026
8d49e74
feat: replace auto generating id to manually
Logaka Feb 2, 2026
4a4b0ab
feat: rewrite retryRestore
Logaka Feb 2, 2026
a1d76c3
Merge branch 'main' into feat/restore-dryRun
ArkuNC Feb 3, 2026
157b3a0
style: detailed OpenApi
Logaka Feb 5, 2026
0b72355
chore: rename entities and dtos
Logaka Feb 5, 2026
d14faa0
style: update OpenApi
Logaka Feb 5, 2026
b4d300b
chore: update entity
Logaka Feb 5, 2026
948be71
refactor: simplify first classifier selection
Logaka Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<OperationAlreadyRunningException> {

@Context
UriInfo uriInfo;

@Override
public Response toResponse(OperationAlreadyRunningException e) {
return buildDefaultResponse(uriInfo, e, Response.Status.CONFLICT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ 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))),
Expand Down Expand Up @@ -159,7 +159,7 @@ public Response getBackupStatus(@Parameter(description = "Unique identifier of t
@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)))
Expand Down Expand Up @@ -230,7 +230,7 @@ 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))),
Expand All @@ -241,7 +241,7 @@ public Response uploadMetadata(
@POST
public Response restoreBackup(@Parameter(description = "Unique identifier of the backup", required = true)
@PathParam("backupName") @NotBlank String backupName,
@RequestBody(description = "Restore request", required = true)
@RequestBody(description = "Restore request")
@Valid RestoreRequest restoreRequest,
@QueryParam("dryRun") @DefaultValue("false") boolean dryRun) {
RestoreResponse response = dbBackupV2Service.restore(backupName, restoreRequest, dryRun);
Expand Down Expand Up @@ -308,14 +308,16 @@ public Response getRestoreStatus(@Parameter(description = "Unique identifier of

@Operation(summary = "Retry restore", description = "Retry a failed restore operation")
@APIResponses({
@APIResponse(responseCode = "200", description = "Restore operation retried successfully",
content = @Content(schema = @Schema(implementation = RestoreResponse.class))),
@APIResponse(responseCode = "202", description = "Restore retry accepted and is being processed",
content = @Content(schema = @Schema(implementation = RestoreResponse.class))),
@APIResponse(responseCode = "401", description = "Authentication is required and has failed or has not been provided"),
@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)))
})
Expand All @@ -324,7 +326,6 @@ public Response getRestoreStatus(@Parameter(description = "Unique identifier of
public Response retryRestore(@Parameter(description = "Unique identifier of the restore operation", required = true)
@PathParam("restoreName")
String restoreName) {
dbBackupV2Service.retryRestore(restoreName);
return Response.ok().build();
return Response.accepted(dbBackupV2Service.retryRestore(restoreName)).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,7 +24,8 @@ public String convertToDatabaseColumn(SortedMap<Object, Object> attribute) {
@Override
public SortedMap<Object, Object> convertToEntityAttribute(String dbData) {
try {
return objectMapper.readValue(dbData, new TypeReference<SortedMap<Object, Object>>() {});
return objectMapper.readValue(dbData, new TypeReference<SortedMap<Object, Object>>() {
});
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -141,6 +137,11 @@ public List<DatabaseRegistry> findAllTransactionalDatabaseRegistries(String name
return databaseRegistryRepository.findAllByNamespaceAndDatabase_BgVersionNull(namespace);
}

@Override
public List<DatabaseRegistry> findAllDatabasesByFilter(List<Filter> filters) {
return databaseRegistryRepository.findAllDatabasesByFilter(filters);
}

@Override
public void delete(DatabaseRegistry databaseRegistry) {
log.debug("Delete logical database with classifier {} and type {}", databaseRegistry.getClassifier(), databaseRegistry.getType());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.netcracker.cloud.dbaas.dto.backupV2;

import com.netcracker.cloud.dbaas.enums.ExternalDatabaseStrategy;
import com.netcracker.cloud.dbaas.utils.validation.BackupGroup;
import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -45,7 +45,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,32 +20,32 @@
@Schema(description = "Response containing backup operation details")
public class BackupResponse {

@NotBlank
@Schema(
description = "Unique identifier 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 = {
"s3-backend"
},
required = true
)
private String storageName;
@NotBlank
private String storageName;
@Schema(
description = "Path to the backup file in the storage",
examples = {
"/backups"
},
required = true
)
@NotBlank
private String blobPath;
@Schema(
description = "How to handle external databases during backup",
Expand All @@ -57,20 +56,20 @@ public class BackupResponse {
)
@NotNull
private ExternalDatabaseStrategy externalDatabaseStrategy;
@NotNull
@Schema(
description = "Whether external databases were skipped during the 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(
Expand All @@ -84,20 +83,23 @@ public class BackupResponse {
"5"
}
)
@NotNull
private Integer total;
@Schema(
description = "Number of databases successfully backed up",
examples = {
"3"
}
)
@NotNull
private Integer completed;
@Schema(
description = "Total size of the backup in bytes",
examples = {
"1073741824"
}
)
@NotNull
private Long size;
@Schema(
description = "Error details if the backup failed",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.netcracker.cloud.dbaas.dto.backupV2;

import com.netcracker.cloud.dbaas.enums.ClassifierType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.SortedMap;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClassifierResponse {
private ClassifierType type;
private String previousDatabase;
private SortedMap<String, Object> classifier;
private SortedMap<String, Object> classifierBeforeMapper;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.netcracker.cloud.dbaas.dto.backupV2;

import com.netcracker.cloud.dbaas.utils.validation.BackupGroup;
import com.netcracker.cloud.dbaas.utils.validation.group.BackupGroup;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

import java.util.ArrayList;
import java.util.List;

@Data
Expand All @@ -23,13 +25,11 @@ public class FilterCriteria {
)
@NotNull(groups = {BackupGroup.class})
@Size(min = 1, groups = {BackupGroup.class})
private List<Filter> filter;
@Schema(
description = "Include databases that match any of the filters in the list"
)
private List<Filter> include;
@Valid
private List<Filter> filter = new ArrayList<>();

@Schema(
description = "Exclude databases that match any of the filters in the list"
)
private List<Filter> exclude;
private List<Filter> exclude = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class RestoreDatabaseResponse {
description = "List of database classifiers. Each classifier is a sorted map of attributes.",
examples = "[{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}]"
)
private List<Map<String, Object>> classifiers;
private List<ClassifierResponse> classifiers;
@Schema(
description = "List of database users",
examples = "[{\"name\":\"username\",\"role\":\"admin\"}"
Expand Down Expand Up @@ -58,7 +58,7 @@ public class RestoreDatabaseResponse {
required = true
)
private String path;
@Schema(description = "Error message if the backup failed", examples = "Restore Not Found")
@Schema(description = "Error message if the backup failed", examples = "Restore Not Found")
private String errorMessage;
@Schema(description = "Timestamp when the restore was created", examples = "2025-11-13T12:34:56Z")
private Instant creationTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.eclipse.microprofile.openapi.annotations.media.Schema;

import java.util.List;
import java.util.SortedMap;

@Data
@NoArgsConstructor
Expand All @@ -19,6 +18,6 @@ public class RestoreExternalDatabaseResponse {
description = "List of database classifiers. Each classifier is a sorted map of attributes.",
examples = "[{\"namespace\":\"namespace\", \"microserviceName\":\"microserviceName\", \"scope\":\"service\"}]"
)
private List<SortedMap<String, Object>> classifiers;
private List<ClassifierResponse> classifiers;
}

Loading
Loading