diff --git a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/SpringBooter.java b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/SpringBooter.java index 437f13347..f609fa29f 100644 --- a/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/SpringBooter.java +++ b/application/src/main/java/com/ericsson/bss/cassandra/ecchronos/application/spring/SpringBooter.java @@ -15,7 +15,9 @@ package com.ericsson.bss.cassandra.ecchronos.application.spring; import com.ericsson.bss.cassandra.ecchronos.rest.MetricsREST; +import com.ericsson.bss.cassandra.ecchronos.rest.OnDemandRepairManagementRESTImpl; import com.ericsson.bss.cassandra.ecchronos.rest.RepairManagementRESTImpl; +import com.ericsson.bss.cassandra.ecchronos.rest.ScheduleRepairManagementRESTImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; @@ -24,7 +26,8 @@ import org.springframework.context.annotation.Import; @SpringBootApplication -@Import(value = {RepairManagementRESTImpl.class, MetricsREST.class}) +@Import(value = {RepairManagementRESTImpl.class, ScheduleRepairManagementRESTImpl.class, + OnDemandRepairManagementRESTImpl.class, MetricsREST.class}) public class SpringBooter extends SpringBootServletInitializer { private static final Logger LOG = LoggerFactory.getLogger(SpringBooter.class); diff --git a/docs/autogenerated/openapi.yaml b/docs/autogenerated/openapi.yaml index a9bc701d5..623e8d215 100644 --- a/docs/autogenerated/openapi.yaml +++ b/docs/autogenerated/openapi.yaml @@ -9,10 +9,10 @@ servers: - url: https://localhost:8080 description: Generated server url tags: +- name: Repair-Management + description: Management of repairs - name: Metrics description: Retrieve metrics about ecChronos -- name: Repair-Management - description: View the status of schedules and repairs as well as run manual repairs - name: Actuator description: Monitor and interact externalDocs: @@ -59,9 +59,9 @@ paths: tags: - Repair-Management summary: Run a manual repair. - description: "Run a manual repair, if 'isLocal' is not provided this will trigger\ + description: "Run a manual repair, if 'isLocal' is not provided this will run\ \ a cluster-wide repair." - operationId: trigger-repair + operationId: run-repair parameters: - name: keyspace in: query diff --git a/ecchronos-binary/src/pylib/ecchronoslib/rest.py b/ecchronos-binary/src/pylib/ecchronoslib/rest.py index be1d71f6c..f88bce532 100644 --- a/ecchronos-binary/src/pylib/ecchronoslib/rest.py +++ b/ecchronos-binary/src/pylib/ecchronoslib/rest.py @@ -124,7 +124,7 @@ class V2RepairSchedulerRequest(RestRequest): v2_repair_status_url = REPAIRS v2_repair_id_status_url = REPAIRS + '/{0}' - v2_repair_trigger_url = REPAIRS + v2_repair_run_url = REPAIRS repair_info_url = PROTOCOL + 'repairInfo' @@ -187,7 +187,7 @@ def list_repairs(self, keyspace=None, table=None, host_id=None): return result def post(self, keyspace=None, table=None, local=False): - request_url = V2RepairSchedulerRequest.v2_repair_trigger_url + request_url = V2RepairSchedulerRequest.v2_repair_run_url if keyspace: request_url += "?keyspace=" + keyspace if table: diff --git a/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementOnDemandRESTComponent.java b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementOnDemandRESTComponent.java new file mode 100644 index 000000000..85267a51f --- /dev/null +++ b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementOnDemandRESTComponent.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest.osgi; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReferenceFactory; +import com.ericsson.bss.cassandra.ecchronos.rest.OnDemandRepairManagementREST; +import com.ericsson.bss.cassandra.ecchronos.rest.OnDemandRepairManagementRESTImpl; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * OSGi component wrapping {@link OnDemandRepairManagementREST} bound with OSGi services. + */ +@Component +public class RepairManagementOnDemandRESTComponent implements OnDemandRepairManagementREST +{ + @Reference (service = OnDemandRepairScheduler.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.STATIC) + private volatile OnDemandRepairScheduler myOnDemandRepairScheduler; + + @Reference(service = TableReferenceFactory.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.STATIC) + private volatile TableReferenceFactory myTableReferenceFactory; + + @Reference(service = ReplicatedTableProvider.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.STATIC) + private volatile ReplicatedTableProvider myReplicatedTableProvider; + + private volatile OnDemandRepairManagementREST myDelegateOnDemandRESTImpl; + + @Activate + public final synchronized void activate() + { + myDelegateOnDemandRESTImpl = new OnDemandRepairManagementRESTImpl(myOnDemandRepairScheduler, + myTableReferenceFactory, myReplicatedTableProvider); + } + + @Override + public final ResponseEntity> getRepairs(final String keyspace, + final String table, + final String hostId) + { + return myDelegateOnDemandRESTImpl.getRepairs(keyspace, table, hostId); + } + + @Override + public final ResponseEntity> getRepairs(final String id, final String hostId) + { + return myDelegateOnDemandRESTImpl.getRepairs(id, hostId); + } + + @Override + public final ResponseEntity> runRepair(final String keyspace, + final String table, + final boolean isLocal) + { + return myDelegateOnDemandRESTImpl.runRepair(keyspace, table, isLocal); + } +} diff --git a/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementRESTComponent.java b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementRESTComponent.java index 023725283..3805f7d58 100644 --- a/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementRESTComponent.java +++ b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementRESTComponent.java @@ -14,11 +14,7 @@ */ package com.ericsson.bss.cassandra.ecchronos.rest.osgi; -import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; import com.ericsson.bss.cassandra.ecchronos.core.utils.RepairStatsProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReferenceFactory; @@ -32,7 +28,6 @@ import org.springframework.http.ResponseEntity; import java.time.Duration; -import java.util.List; /** * OSGi component wrapping {@link RepairManagementREST} bound with OSGi services. @@ -40,16 +35,6 @@ @Component public class RepairManagementRESTComponent implements RepairManagementREST { - @Reference (service = RepairScheduler.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.STATIC) - private volatile RepairScheduler myRepairScheduler; - - @Reference (service = OnDemandRepairScheduler.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.STATIC) - private volatile OnDemandRepairScheduler myOnDemandRepairScheduler; - @Reference(service = TableReferenceFactory.class, cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) @@ -69,42 +54,8 @@ public class RepairManagementRESTComponent implements RepairManagementREST @Activate public final synchronized void activate() { - myDelegateRESTImpl = new RepairManagementRESTImpl(myRepairScheduler, myOnDemandRepairScheduler, - myTableReferenceFactory, myReplicatedTableProvider, myRepairStatsProvider); - } - - @Override - public final ResponseEntity> getRepairs(final String keyspace, - final String table, - final String hostId) - { - return myDelegateRESTImpl.getRepairs(keyspace, table, hostId); - } - - @Override - public final ResponseEntity> getRepairs(final String id, final String hostId) - { - return myDelegateRESTImpl.getRepairs(id, hostId); - } - - @Override - public final ResponseEntity> getSchedules(final String keyspace, final String table) - { - return myDelegateRESTImpl.getSchedules(keyspace, table); - } - - @Override - public final ResponseEntity getSchedules(final String id, final boolean full) - { - return myDelegateRESTImpl.getSchedules(id, full); - } - - @Override - public final ResponseEntity> triggerRepair(final String keyspace, - final String table, - final boolean isLocal) - { - return myDelegateRESTImpl.triggerRepair(keyspace, table, isLocal); + myDelegateRESTImpl = new RepairManagementRESTImpl(myTableReferenceFactory, + myReplicatedTableProvider, myRepairStatsProvider); } @Override diff --git a/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementScheduleRESTComponent.java b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementScheduleRESTComponent.java new file mode 100644 index 000000000..7fb9e837a --- /dev/null +++ b/rest.osgi/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/osgi/RepairManagementScheduleRESTComponent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest.osgi; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import com.ericsson.bss.cassandra.ecchronos.rest.ScheduleRepairManagementREST; +import com.ericsson.bss.cassandra.ecchronos.rest.ScheduleRepairManagementRESTImpl; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * OSGi component wrapping {@link ScheduleRepairManagementREST} bound with OSGi services. + */ +@Component +public class RepairManagementScheduleRESTComponent implements ScheduleRepairManagementREST +{ + @Reference (service = RepairScheduler.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.STATIC) + private volatile RepairScheduler myRepairScheduler; + + private volatile ScheduleRepairManagementREST myDelegateScheduleRESTImpl; + + @Activate + public final synchronized void activate() + { + myDelegateScheduleRESTImpl = new ScheduleRepairManagementRESTImpl(myRepairScheduler); + } + + @Override + public final ResponseEntity> getSchedules(final String keyspace, final String table) + { + return myDelegateScheduleRESTImpl.getSchedules(keyspace, table); + } + + @Override + public final ResponseEntity getSchedules(final String id, final boolean full) + { + return myDelegateScheduleRESTImpl.getSchedules(id, full); + } +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java new file mode 100644 index 000000000..4ec20baa6 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementREST.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * On Demand Repair REST interface. + * + * Whenever the interface is changed it must be reflected in docs. + */ +public interface OnDemandRepairManagementREST +{ + /** + * Get a list of on demand repairs. Will fetch all if no keyspace or table is specified. + * + * @param keyspace The keyspace of the table (optional) + * @param table The table to get status of (optional) + * @param hostId The hostId of the on demand repair (optional) + * @return A list of JSON representations of {@link OnDemandRepair} + */ + ResponseEntity> getRepairs(String keyspace, String table, String hostId); + /** + * Get a list of on demand repairs associated with a specific id. + * + * @param id The id of the on demand repair + * @param hostId The hostId of the on demand repair (optional) + * @return A list of JSON representations of {@link OnDemandRepair} + */ + ResponseEntity> getRepairs(String id, String hostId); + + /** + * Schedule an on demand repair to be run on a specific table. + * + * @param keyspace The keyspace of the table + * @param table The table + * @param isLocal If repair should be only run for the local node (optional) + * @return A JSON representation of {@link OnDemandRepair} + */ + ResponseEntity> runRepair(String keyspace, String table, boolean isLocal); +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java new file mode 100644 index 000000000..a7f9d985a --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/OnDemandRepairManagementRESTImpl.java @@ -0,0 +1,271 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.exceptions.EcChronosException; +import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReferenceFactory; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * When updating the path it should also be updated in the OSGi component. + */ +@Tag(name = "Repair-Management", description = "Management of repairs") +@RestController +public class OnDemandRepairManagementRESTImpl implements OnDemandRepairManagementREST +{ + private final OnDemandRepairScheduler myOnDemandRepairScheduler; + + private final TableReferenceFactory myTableReferenceFactory; + + private final ReplicatedTableProvider myReplicatedTableProvider; + + public OnDemandRepairManagementRESTImpl(final OnDemandRepairScheduler demandRepairScheduler, + final TableReferenceFactory tableReferenceFactory, + final ReplicatedTableProvider replicatedTableProvider) + { + myOnDemandRepairScheduler = demandRepairScheduler; + myTableReferenceFactory = tableReferenceFactory; + myReplicatedTableProvider = replicatedTableProvider; + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-repairs", description = "Get manual repairs which are running/completed/failed.", + summary = "Get manual repairs.") + public final ResponseEntity> getRepairs( + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the keyspace, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the table.") + final String table, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the hostId.") + final String hostId) + { + return ResponseEntity.ok(getListOfOnDemandRepairs(keyspace, table, hostId)); + } + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-repairs-by-id", + description = "Get manual repairs matching the id which are running/completed/failed.", + summary = "Get manual repairs matching the id.") + public final ResponseEntity> getRepairs( + @PathVariable + @Parameter(description = "Only return repairs matching the id.") + final String id, + @RequestParam(required = false) + @Parameter(description = "Only return repairs matching the hostId.") + final String hostId) + { + return ResponseEntity.ok(getListOfOnDemandRepairs(id, hostId)); + } + + @Override + @PostMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "run-repair", + description = "Run a manual repair, if 'isLocal' is not provided this will run a cluster-wide repair.", + summary = "Run a manual repair.") + public final ResponseEntity> runRepair( + @RequestParam(required = false) + @Parameter(description = "The keyspace to run repair for, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "The table to run repair for.") + final String table, + @RequestParam(required = false) + @Parameter(description = "Decides if the repair should be only for the local node, i.e not cluster-wide.") + final boolean isLocal) + { + return ResponseEntity.ok(getListOfOnDemandRepairs(keyspace, table, isLocal)); + } + + + private List getListOfOnDemandRepairs(final String keyspace, final String table, + final String hostId) + { + if (keyspace != null) + { + if (table != null) + { + if (hostId == null) + { + return getClusterWideOnDemandJobs(forTableOnDemand(keyspace, table)); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs(job -> keyspace.equals(job.getTableReference().getKeyspace()) + && table.equals(job.getTableReference().getTable()) + && host.equals(job.getHostId())); + } + if (hostId == null) + { + return getClusterWideOnDemandJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace())); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace()) + && host.equals(job.getHostId())); + } + else if (table == null) + { + if (hostId == null) + { + return getClusterWideOnDemandJobs(job -> true); + } + UUID host = parseIdOrThrow(hostId); + return getClusterWideOnDemandJobs(job -> host.equals(job.getHostId())); + } + throw new ResponseStatusException(BAD_REQUEST); + } + + private List getListOfOnDemandRepairs(final String id, final String hostId) + { + UUID uuid = parseIdOrThrow(id); + if (hostId == null) + { + List repairJobs = getClusterWideOnDemandJobs( + job -> uuid.equals(job.getId())); + if (repairJobs.isEmpty()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return repairJobs; + } + UUID host = parseIdOrThrow(hostId); + List repairJobs = getClusterWideOnDemandJobs(job -> uuid.equals(job.getId()) + && host.equals(job.getHostId())); + if (repairJobs.isEmpty()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return repairJobs; + } + + private List getListOfOnDemandRepairs(final String keyspace, final String table, + final boolean isLocal) + { + try + { + List onDemandRepairs; + if (keyspace != null) + { + if (table != null) + { + TableReference tableReference = myTableReferenceFactory.forTable(keyspace, table); + if (tableReference == null) + { + throw new ResponseStatusException(NOT_FOUND, + "Table " + keyspace + "." + table + " does not exist"); + } + onDemandRepairs = runLocalOrCluster( + isLocal, Collections.singleton(myTableReferenceFactory.forTable(keyspace, table))); + } + else + { + onDemandRepairs = runLocalOrCluster(isLocal, myTableReferenceFactory.forKeyspace(keyspace)); + } + } + else + { + if (table != null) + { + throw new ResponseStatusException(BAD_REQUEST, "Keyspace must be provided if table is provided"); + } + onDemandRepairs = runLocalOrCluster(isLocal, myTableReferenceFactory.forCluster()); + } + return onDemandRepairs; + } + catch (EcChronosException e) + { + throw new ResponseStatusException(NOT_FOUND, NOT_FOUND.getReasonPhrase(), e); + } + } + + private static Predicate forTableOnDemand(final String keyspace, final String table) + { + return tableView -> + { + TableReference tableReference = tableView.getTableReference(); + return tableReference.getKeyspace().equals(keyspace) + && tableReference.getTable().equals(table); + }; + } + + private List getClusterWideOnDemandJobs(final Predicate filter) + { + return myOnDemandRepairScheduler.getAllClusterWideRepairJobs().stream() + .filter(filter) + .map(OnDemandRepair::new) + .collect(Collectors.toList()); + } + + private List runLocalOrCluster(final boolean isLocal, final Set tables) + throws EcChronosException + { + List onDemandRepairs = new ArrayList<>(); + for (TableReference tableReference : tables) + { + if (isLocal) + { + if (myReplicatedTableProvider.accept(tableReference.getKeyspace())) + { + onDemandRepairs.add(new OnDemandRepair(myOnDemandRepairScheduler.scheduleJob(tableReference))); + } + } + else + { + if (myReplicatedTableProvider.accept(tableReference.getKeyspace())) + { + List repairJobView = myOnDemandRepairScheduler.scheduleClusterWideJob( + tableReference); + onDemandRepairs.addAll( + repairJobView.stream().map(OnDemandRepair::new).collect(Collectors.toList())); + } + } + } + return onDemandRepairs; + } +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java index 8c8ce53e5..d7532bf40 100644 --- a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementREST.java @@ -14,67 +14,18 @@ */ package com.ericsson.bss.cassandra.ecchronos.rest; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; import org.springframework.http.ResponseEntity; import java.time.Duration; -import java.util.List; /** - * Repair scheduler rest interface. + * Repair REST interface. * * Whenever the interface is changed it must be reflected in docs. */ public interface RepairManagementREST { - /** - * Get a list of on demand repairs. Will fetch all if no keyspace or table is specified. - * - * @param keyspace The keyspace of the table (optional) - * @param table The table to get status of (optional) - * @param hostId The hostId of the on demand repair (optional) - * @return A list of JSON representations of {@link OnDemandRepair} - */ - ResponseEntity> getRepairs(String keyspace, String table, String hostId); - /** - * Get a list of on demand repairs associate with a specific id. - * - * @param id The id of the on demand repair - * @param hostId The hostId of the on demand repair (optional) - * @return A list of JSON representations of {@link OnDemandRepair} - */ - ResponseEntity> getRepairs(String id, String hostId); - - /** - * Get a list of schedules. Will fetch all if no keyspace or table is specified. - * - * @param keyspace The keyspace of the table (optional) - * @param table The table to get status of (optional) - * @return A list of JSON representations of {@link Schedule} - */ - ResponseEntity> getSchedules(String keyspace, String table); - - /** - * Get schedule with a specific id. - * - * @param id The id of the schedule - * @param full Whether to include token range information. - * @return A JSON representation of {@link Schedule} - */ - ResponseEntity getSchedules(String id, boolean full); - - /** - * Schedule an on demand repair to be run on a specific table. - * - * @param keyspace The keyspace of the table - * @param table The table - * @param isLocal If repair should be only run for the local node (optional) - * @return A JSON representation of {@link OnDemandRepair} - */ - ResponseEntity> triggerRepair(String keyspace, String table, boolean isLocal); - /** * Get repair information for a specific table. * diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java index 9a8870b40..ec47f60fb 100644 --- a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RepairManagementRESTImpl.java @@ -15,14 +15,8 @@ package com.ericsson.bss.cassandra.ecchronos.rest; import com.ericsson.bss.cassandra.ecchronos.core.exceptions.EcChronosException; -import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairJobView; -import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.ScheduledRepairJobView; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; import com.ericsson.bss.cassandra.ecchronos.core.utils.RepairStatsProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; @@ -39,8 +33,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; @@ -49,20 +41,18 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.getDefaultDurationOrProvided; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; /** * When updating the path it should also be updated in the OSGi component. */ -@Tag(name = "Repair-Management", description = "View the status of schedules and repairs as well as run manual repairs") +@Tag(name = "Repair-Management", description = "Management of repairs") @RestController @OpenAPIDefinition(info = @Info( title = "REST API", @@ -70,19 +60,8 @@ name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0"), version = "1.0.0")) -@SuppressWarnings("PMD.GodClass") // We might want to refactor public class RepairManagementRESTImpl implements RepairManagementREST { - private static final String ROOT = "/repair-management/"; - private static final String PROTOCOL_VERSION = "v2"; - private static final String ENDPOINT_PREFIX = ROOT + PROTOCOL_VERSION; - - @Autowired - private final RepairScheduler myRepairScheduler; - - @Autowired - private final OnDemandRepairScheduler myOnDemandRepairScheduler; - @Autowired private final TableReferenceFactory myTableReferenceFactory; @@ -92,238 +71,17 @@ public class RepairManagementRESTImpl implements RepairManagementREST @Autowired private final RepairStatsProvider myRepairStatsProvider; - public RepairManagementRESTImpl(final RepairScheduler repairScheduler, - final OnDemandRepairScheduler demandRepairScheduler, - final TableReferenceFactory tableReferenceFactory, + public RepairManagementRESTImpl(final TableReferenceFactory tableReferenceFactory, final ReplicatedTableProvider replicatedTableProvider, final RepairStatsProvider repairStatsProvider) { - myRepairScheduler = repairScheduler; - myOnDemandRepairScheduler = demandRepairScheduler; myTableReferenceFactory = tableReferenceFactory; myReplicatedTableProvider = replicatedTableProvider; myRepairStatsProvider = repairStatsProvider; } @Override - @GetMapping(value = ENDPOINT_PREFIX + "/schedules", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "get-schedules", description = "Get schedules", summary = "Get schedules") - public final ResponseEntity> getSchedules( - @RequestParam(required = false) - @Parameter(description = "Filter schedules based on this keyspace, mandatory if 'table' is provided.") - final String keyspace, - @RequestParam(required = false) - @Parameter(description = "Filter schedules based on this table.") - final String table) - { - if (keyspace != null) - { - if (table != null) - { - List repairJobs = getScheduledRepairJobs(forTableSchedule(keyspace, table)); - return ResponseEntity.ok(repairJobs); - } - List repairJobs = getScheduledRepairJobs( - job -> keyspace.equals(job.getTableReference().getKeyspace())); - return ResponseEntity.ok(repairJobs); - } - else if (table == null) - { - List repairJobs = getScheduledRepairJobs(job -> true); - return ResponseEntity.ok(repairJobs); - } - throw new ResponseStatusException(BAD_REQUEST); - } - - @Override - @GetMapping(value = ENDPOINT_PREFIX + "/schedules/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "get-schedules-by-id", description = "Get schedules matching the id.", - summary = "Get schedules matching the id.") - public final ResponseEntity getSchedules( - @PathVariable - @Parameter(description = "The id of the schedule.") - final String id, - @RequestParam(required = false) - @Parameter(description = "Decides if a 'full schedule' should be returned.") - final boolean full) - { - - UUID uuid; - try - { - uuid = UUID.fromString(id); - } - catch (IllegalArgumentException e) - { - throw new ResponseStatusException(BAD_REQUEST, BAD_REQUEST.getReasonPhrase(), e); - } - Optional repairJobView = getScheduleView(uuid); - if (!repairJobView.isPresent()) - { - throw new ResponseStatusException(NOT_FOUND); - } - return ResponseEntity.ok(new Schedule(repairJobView.get(), full)); - - } - - @Override - @GetMapping(value = ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "get-repairs", description = "Get manual repairs which are running/completed/failed.", - summary = "Get manual repairs.") - public final ResponseEntity> getRepairs( - @RequestParam(required = false) - @Parameter(description = "Only return repairs matching the keyspace, mandatory if 'table' is provided.") - final String keyspace, - @RequestParam(required = false) - @Parameter(description = "Only return repairs matching the table.") - final String table, - @RequestParam(required = false) - @Parameter(description = "Only return repairs matching the hostId.") - final String hostId) - { - if (keyspace != null) - { - if (table != null) - { - if (hostId == null) - { - List repairJobs = getClusterWideOnDemandJobs(forTableOnDemand(keyspace, table)); - return ResponseEntity.ok(repairJobs); - } - UUID host = parseIdOrThrow(hostId); - List repairJobs = - getClusterWideOnDemandJobs(job -> keyspace.equals(job.getTableReference().getKeyspace()) - && table.equals(job.getTableReference().getTable()) - && host.equals(job.getHostId())); - return ResponseEntity.ok(repairJobs); - } - if (hostId == null) - { - List repairJobs = getClusterWideOnDemandJobs( - job -> keyspace.equals(job.getTableReference().getKeyspace())); - return ResponseEntity.ok(repairJobs); - } - UUID host = parseIdOrThrow(hostId); - List repairJobs = getClusterWideOnDemandJobs( - job -> keyspace.equals(job.getTableReference().getKeyspace()) - && host.equals(job.getHostId())); - return ResponseEntity.ok(repairJobs); - } - else if (table == null) - { - if (hostId == null) - { - List repairJobs = getClusterWideOnDemandJobs(job -> true); - return ResponseEntity.ok(repairJobs); - } - UUID host = parseIdOrThrow(hostId); - List repairJobs = getClusterWideOnDemandJobs(job -> host.equals(job.getHostId())); - return ResponseEntity.ok(repairJobs); - } - throw new ResponseStatusException(BAD_REQUEST); - } - - @Override - @GetMapping(value = ENDPOINT_PREFIX + "/repairs/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "get-repairs-by-id", - description = "Get manual repairs matching the id which are running/completed/failed.", - summary = "Get manual repairs matching the id.") - public final ResponseEntity> getRepairs( - @PathVariable - @Parameter(description = "Only return repairs matching the id.") - final String id, - @RequestParam(required = false) - @Parameter(description = "Only return repairs matching the hostId.") - final String hostId) - { - UUID uuid = parseIdOrThrow(id); - if (hostId == null) - { - List repairJobs = getClusterWideOnDemandJobs(job -> uuid.equals(job.getId())); - if (repairJobs.isEmpty()) - { - throw new ResponseStatusException(NOT_FOUND); - } - return ResponseEntity.ok(repairJobs); - } - UUID host = parseIdOrThrow(hostId); - List repairJobs = getClusterWideOnDemandJobs(job -> uuid.equals(job.getId()) - && host.equals(job.getHostId())); - if (repairJobs.isEmpty()) - { - throw new ResponseStatusException(NOT_FOUND); - } - return ResponseEntity.ok(repairJobs); - } - - private UUID parseIdOrThrow(final String id) - { - try - { - UUID uuid = UUID.fromString(id); - return uuid; - } - catch (IllegalArgumentException e) - { - throw new ResponseStatusException(BAD_REQUEST, BAD_REQUEST.getReasonPhrase(), e); - } - } - - @Override - @PostMapping(value = ENDPOINT_PREFIX + "/repairs", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "trigger-repair", - description = "Run a manual repair, if 'isLocal' is not provided this will trigger a cluster-wide repair.", - summary = "Run a manual repair.") - public final ResponseEntity> triggerRepair( - @RequestParam(required = false) - @Parameter(description = "The keyspace to run repair for, mandatory if 'table' is provided.") - final String keyspace, - @RequestParam(required = false) - @Parameter(description = "The table to run repair for.") - final String table, - @RequestParam(required = false) - @Parameter(description = "Decides if the repair should be only for the local node, i.e not cluster-wide.") - final boolean isLocal) - { - try - { - List onDemandRepairs; - if (keyspace != null) - { - if (table != null) - { - TableReference tableReference = myTableReferenceFactory.forTable(keyspace, table); - if (tableReference == null) - { - throw new ResponseStatusException(NOT_FOUND, - "Table " + keyspace + "." + table + " does not exist"); - } - onDemandRepairs = runLocalOrCluster(isLocal, - Collections.singleton(myTableReferenceFactory.forTable(keyspace, table))); - } - else - { - onDemandRepairs = runLocalOrCluster(isLocal, myTableReferenceFactory.forKeyspace(keyspace)); - } - } - else - { - if (table != null) - { - throw new ResponseStatusException(BAD_REQUEST, "Keyspace must be provided if table is provided"); - } - onDemandRepairs = runLocalOrCluster(isLocal, myTableReferenceFactory.forCluster()); - } - return ResponseEntity.ok(onDemandRepairs); - } - catch (EcChronosException e) - { - throw new ResponseStatusException(NOT_FOUND, NOT_FOUND.getReasonPhrase(), e); - } - } - - @Override - @GetMapping(value = ENDPOINT_PREFIX + "/repairInfo", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/repairInfo", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(operationId = "get-repair-info", description = "Get repair information, if keyspace and table are provided while duration and since are" + " not, the duration will default to GC_GRACE_SECONDS of the table. " @@ -352,10 +110,17 @@ public final ResponseEntity getRepairInfo( @RequestParam(required = false) @Parameter(description = "Decides if the repair-info should be calculated for the local node only.") final boolean isLocal) + { + return ResponseEntity.ok(fetchRepairInfo(keyspace, table, since, duration, isLocal)); + } + + private RepairInfo fetchRepairInfo(final String keyspace, final String table, + final Long since, final Duration duration, final boolean isLocal) { try { RepairInfo repairInfo; + Duration actualDuration = duration; if (keyspace != null) { if (table != null) @@ -366,13 +131,14 @@ public final ResponseEntity getRepairInfo( throw new ResponseStatusException(NOT_FOUND, "Table " + keyspace + "." + table + " does not exist"); } - Duration singleTableDuration = getDefaultDurationOrProvided(tableReference, duration, since); - repairInfo = getRepairInfo(Collections.singleton(tableReference), since, - singleTableDuration, isLocal); + actualDuration = getDefaultDurationOrProvided(tableReference, duration, since); + repairInfo = createRepairInfo(Collections.singleton(tableReference), since, actualDuration, + isLocal); } else { - repairInfo = getRepairInfo(myTableReferenceFactory.forKeyspace(keyspace), since, duration, isLocal); + repairInfo = createRepairInfo(myTableReferenceFactory.forKeyspace(keyspace), since, actualDuration, + isLocal); } } else @@ -381,9 +147,9 @@ public final ResponseEntity getRepairInfo( { throw new ResponseStatusException(BAD_REQUEST, "Keyspace must be provided if table is provided"); } - repairInfo = getRepairInfo(myTableReferenceFactory.forCluster(), since, duration, isLocal); + repairInfo = createRepairInfo(myTableReferenceFactory.forCluster(), since, actualDuration, isLocal); } - return ResponseEntity.ok(repairInfo); + return repairInfo; } catch (EcChronosException e) { @@ -391,19 +157,8 @@ public final ResponseEntity getRepairInfo( } } - private Duration getDefaultDurationOrProvided(final TableReference tableReference, final Duration duration, - final Long since) - { - Duration singleTableDuration = duration; - if (duration == null && since == null) - { - singleTableDuration = Duration.ofSeconds(tableReference.getGcGraceSeconds()); - } - return singleTableDuration; - } - - private RepairInfo getRepairInfo(final Set tables, final Long since, final Duration duration, - final boolean isLocal) + private RepairInfo createRepairInfo(final Set tables, final Long since, + final Duration duration, final boolean isLocal) { long toTime = System.currentTimeMillis(); long sinceTime; @@ -428,6 +183,7 @@ else if (duration != null) throw new ResponseStatusException(BAD_REQUEST, "'to' (" + toTime + ") is before 'since' (" + sinceTime + ")"); } + List repairStats = new ArrayList<>(); for (TableReference table : tables) { @@ -439,72 +195,4 @@ else if (duration != null) return new RepairInfo(sinceTime, toTime, repairStats); } - private List runLocalOrCluster(final boolean isLocal, final Set tables) - throws EcChronosException - { - List onDemandRepairs = new ArrayList<>(); - for (TableReference tableReference : tables) - { - if (isLocal) - { - if (myReplicatedTableProvider.accept(tableReference.getKeyspace())) - { - onDemandRepairs.add(new OnDemandRepair(myOnDemandRepairScheduler.scheduleJob(tableReference))); - } - } - else - { - if (myReplicatedTableProvider.accept(tableReference.getKeyspace())) - { - List repairJobView = myOnDemandRepairScheduler.scheduleClusterWideJob( - tableReference); - onDemandRepairs.addAll( - repairJobView.stream().map(OnDemandRepair::new).collect(Collectors.toList())); - } - } - } - return onDemandRepairs; - } - - private List getScheduledRepairJobs(final Predicate filter) - { - return myRepairScheduler.getCurrentRepairJobs().stream() - .filter(filter) - .map(Schedule::new) - .collect(Collectors.toList()); - } - - private List getClusterWideOnDemandJobs(final Predicate filter) - { - return myOnDemandRepairScheduler.getAllClusterWideRepairJobs().stream() - .filter(filter) - .map(OnDemandRepair::new) - .collect(Collectors.toList()); - } - - private Optional getScheduleView(final UUID id) - { - return myRepairScheduler.getCurrentRepairJobs().stream() - .filter(job -> job.getId().equals(id)).findFirst(); - } - - private Predicate forTableSchedule(final String keyspace, final String table) - { - return tableView -> - { - TableReference tableReference = tableView.getTableReference(); - return tableReference.getKeyspace().equals(keyspace) - && tableReference.getTable().equals(table); - }; - } - - private Predicate forTableOnDemand(final String keyspace, final String table) - { - return tableView -> - { - TableReference tableReference = tableView.getTableReference(); - return tableReference.getKeyspace().equals(keyspace) - && tableReference.getTable().equals(table); - }; - } } diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java new file mode 100644 index 000000000..1b4a9cf88 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.UUID; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * Helper class that contains methods to parse REST information. + */ +public final class RestUtils +{ + public static final String ROOT = "/repair-management/"; + public static final String PROTOCOL_VERSION = "v2"; + public static final String REPAIR_MANAGEMENT_ENDPOINT_PREFIX = ROOT + PROTOCOL_VERSION; + + private RestUtils() + { + // Utility Class + } + + public static UUID parseIdOrThrow(final String id) + { + try + { + UUID uuid = UUID.fromString(id); + return uuid; + } + catch (IllegalArgumentException e) + { + throw new ResponseStatusException(BAD_REQUEST, BAD_REQUEST.getReasonPhrase(), e); + } + } + + /** + * Fetches duration provided. + * if no duration and since are provided, it will fetch the table default + * + * @param tableReference the table to fetch the default from + * @param duration provided duration + * @param since provided since + * @return the duration + */ + public static Duration getDefaultDurationOrProvided(final TableReference tableReference, final Duration duration, + final Long since) + { + Duration singleTableDuration = duration; + if (duration == null && since == null) + { + singleTableDuration = Duration.ofSeconds(tableReference.getGcGraceSeconds()); + } + return singleTableDuration; + } +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java new file mode 100644 index 000000000..213d0f998 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementREST.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * Schedule REST interface. + * + * Whenever the interface is changed it must be reflected in docs. + */ +public interface ScheduleRepairManagementREST +{ + /** + * Get a list of schedules. Will fetch all if no keyspace or table is specified. + * + * @param keyspace The keyspace of the table (optional) + * @param table The table to get status of (optional) + * @return A list of JSON representations of {@link Schedule} + */ + ResponseEntity> getSchedules(String keyspace, String table); + + /** + * Get schedule with a specific id. + * + * @param id The id of the schedule + * @param full Whether to include token range information. + * @return A JSON representation of {@link Schedule} + */ + ResponseEntity getSchedules(String id, boolean full); +} diff --git a/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java new file mode 100644 index 000000000..d8112b9b2 --- /dev/null +++ b/rest/src/main/java/com/ericsson/bss/cassandra/ecchronos/rest/ScheduleRepairManagementRESTImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.ScheduledRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.REPAIR_MANAGEMENT_ENDPOINT_PREFIX; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * When updating the path it should also be updated in the OSGi component. + */ +@Tag(name = "Repair-Management", description = "Management of repairs") +@RestController +public class ScheduleRepairManagementRESTImpl implements ScheduleRepairManagementREST +{ + @Autowired + private final RepairScheduler myRepairScheduler; + + public ScheduleRepairManagementRESTImpl(final RepairScheduler repairScheduler) + { + myRepairScheduler = repairScheduler; + } + + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/schedules", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-schedules", description = "Get schedules", summary = "Get schedules") + public final ResponseEntity> getSchedules( + @RequestParam(required = false) + @Parameter(description = "Filter schedules based on this keyspace, mandatory if 'table' is provided.") + final String keyspace, + @RequestParam(required = false) + @Parameter(description = "Filter schedules based on this table.") + final String table) + { + return ResponseEntity.ok(getListOfSchedules(keyspace, table)); + } + + + @Override + @GetMapping(value = REPAIR_MANAGEMENT_ENDPOINT_PREFIX + "/schedules/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "get-schedules-by-id", description = "Get schedules matching the id.", + summary = "Get schedules matching the id.") + public final ResponseEntity getSchedules( + @PathVariable + @Parameter(description = "The id of the schedule.") + final String id, + @RequestParam(required = false) + @Parameter(description = "Decides if a 'full schedule' should be returned.") + final boolean full) + { + return ResponseEntity.ok(getScheduleView(id, full)); + + } + + private List getScheduledRepairJobs(final Predicate filter) + { + return myRepairScheduler.getCurrentRepairJobs().stream() + .filter(filter) + .map(Schedule::new) + .collect(Collectors.toList()); + } + + private Schedule getScheduleView(final String id, final boolean full) + { + UUID uuid = parseIdOrThrow(id); + Optional view = myRepairScheduler.getCurrentRepairJobs().stream() + .filter(job -> job.getId().equals(uuid)).findFirst(); + if (!view.isPresent()) + { + throw new ResponseStatusException(NOT_FOUND); + } + return new Schedule(view.get(), full); + } + + private List getListOfSchedules(final String keyspace, final String table) + { + if (keyspace != null) + { + if (table != null) + { + return getScheduledRepairJobs(forTableSchedule(keyspace, table)); + } + return getScheduledRepairJobs( + job -> keyspace.equals(job.getTableReference().getKeyspace())); + } + else if (table == null) + { + return getScheduledRepairJobs(job -> true); + } + throw new ResponseStatusException(BAD_REQUEST); + } + + private static Predicate forTableSchedule(final String keyspace, final String table) + { + return tableView -> + { + TableReference tableReference = tableView.getTableReference(); + return tableReference.getKeyspace().equals(keyspace) + && tableReference.getTable().equals(table); + }; + } +} diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java new file mode 100644 index 000000000..2865e87d3 --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/RestUtilsTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import org.junit.Test; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Duration; +import java.util.UUID; + +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.getDefaultDurationOrProvided; +import static com.ericsson.bss.cassandra.ecchronos.rest.RestUtils.parseIdOrThrow; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestUtilsTest +{ + @Test + public void testParseUuid() + { + UUID uuid = UUID.randomUUID(); + UUID processedId = parseIdOrThrow(uuid.toString()); + assertThat(uuid).isEqualTo(processedId); + } + + @Test + public void testParseUuidFails() + { + String incorrectId = "incorrectId"; + catchThrowableOfType(() -> parseIdOrThrow(incorrectId), ResponseStatusException.class); + } + + @Test + public void testDurationWithSince() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long since = 1245L; + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, duration, since); + assertThat(processedDuration).isEqualTo(duration); + } + + @Test + public void testDefaultDurationDefault() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, null, null); + assertThat(processedDuration).isEqualTo(Duration.ofMillis(12000)); + } + + @Test + public void testDurationWithoutSince() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long durationInMs = 1000L; + Duration duration = Duration.ofMillis(durationInMs); + Duration processedDuration = getDefaultDurationOrProvided(tableReference, duration, null); + assertThat(processedDuration).isEqualTo(duration); + } + + @Test + public void testDurationWithoutDurationReturnsNull() + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getGcGraceSeconds()).thenReturn(12); + long since = 1245L; + Duration processedDuration = getDefaultDurationOrProvided(tableReference, null, since); + assertThat(processedDuration).isNull(); + } +} diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java new file mode 100644 index 000000000..01128679d --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestOnDemandRepairManagementRESTImpl.java @@ -0,0 +1,343 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.exceptions.EcChronosException; +import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; +import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReferenceFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static com.ericsson.bss.cassandra.ecchronos.rest.TestRepairManagementRESTImpl.DEFAULT_GC_GRACE_SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestOnDemandRepairManagementRESTImpl +{ + @Mock + private OnDemandRepairScheduler myOnDemandRepairScheduler; + + @Mock + private ReplicatedTableProvider myReplicatedTableProvider; + + @Mock + private TableReferenceFactory myTableReferenceFactory; + + private OnDemandRepairManagementREST OnDemandRest; + + @Before + public void setupMocks() + { + OnDemandRest = new OnDemandRepairManagementRESTImpl(myOnDemandRepairScheduler, + myTableReferenceFactory, myReplicatedTableProvider); + } + + @Test + public void testGetNoRepairs() + { + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(new ArrayList<>()); + + ResponseEntity> response; + + response = OnDemandRest.getRepairs(null, null, null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", null, null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", "tb", null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = OnDemandRest.getRepairs("ks", "tb", UUID.randomUUID().toString()); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void testGetRepairs() + { + UUID expectedId = UUID.randomUUID(); + UUID expectedHostId = UUID.randomUUID(); + TableReference tableReference1 = mockTableReference("ks", "tb"); + OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, 1234L); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 2345L); + OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 3456L); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) + .collect(Collectors.toList()); + + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); + + ResponseEntity> response = OnDemandRest.getRepairs(null, null, null); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs(null, null, expectedHostId.toString()); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", null, null); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", null, expectedHostId.toString()); + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", null); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", expectedHostId.toString()); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "tb", UUID.randomUUID().toString()); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("wrong", "tb", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("ks", "wrong", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = OnDemandRest.getRepairs("wrong", "wrong", null); + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetRepairWithId() + { + UUID expectedId = UUID.randomUUID(); + UUID expectedHostId = UUID.randomUUID(); + TableReference tableReference1 = mockTableReference("ks", "tb"); + OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, 1234L); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 2345L); + OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, + 3456L); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) + .collect(Collectors.toList()); + + when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); + ResponseEntity> response = null; + try + { + response = OnDemandRest.getRepairs(UUID.randomUUID().toString(), expectedHostId.toString()); + } + catch (ResponseStatusException e) + { + assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); + } + assertThat(response).isNull(); + response = OnDemandRest.getRepairs(expectedId.toString(), expectedHostId.toString()); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = null; + try + { + response = OnDemandRest.getRepairs(UUID.randomUUID().toString(), null); + } + catch (ResponseStatusException e) + { + assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); + } + assertThat(response).isNull(); + response = OnDemandRest.getRepairs(expectedId.toString(), null); + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepair() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference = mockTableReference("ks", "tb"); + OnDemandRepairJobView localRepairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt); + List localExpectedResponse = Collections.singletonList(new OnDemandRepair(localRepairJobView)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference)).thenReturn(localRepairJobView); + when(myReplicatedTableProvider.accept("ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair("ks", "tb", true); + + assertThat(response.getBody()).isEqualTo(localExpectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + OnDemandRepairJobView repairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt); + List expectedResponse = Collections.singletonList(new OnDemandRepair(repairJobView)); + + when(myOnDemandRepairScheduler.scheduleClusterWideJob(tableReference)).thenReturn( + Collections.singletonList(repairJobView)); + response = OnDemandRest.runRepair("ks", "tb", false); + + assertThat(response.getBody()).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairOnlyKeyspace() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("ks", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt); + TableReference tableReference2 = mockTableReference("ks", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt); + TableReference tableReference3 = mockTableReference("ks", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt); + + Set tableReferencesForKeyspace = new HashSet<>(); + tableReferencesForKeyspace.add(tableReference1); + tableReferencesForKeyspace.add(tableReference2); + tableReferencesForKeyspace.add(tableReference3); + when(myTableReferenceFactory.forKeyspace("ks")).thenReturn(tableReferencesForKeyspace); + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3)).thenReturn(repairJobView3); + when(myReplicatedTableProvider.accept("ks")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair("ks", null, true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testRunRepairNoKeyspaceNoTable() throws EcChronosException + { + UUID id = UUID.randomUUID(); + UUID hostId = UUID.randomUUID(); + long completedAt = 1234L; + TableReference tableReference1 = mockTableReference("keyspace1", "table1"); + OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt); + TableReference tableReference2 = mockTableReference("keyspace1", "table2"); + OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt); + TableReference tableReference3 = mockTableReference("keyspace1", "table3"); + OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt); + TableReference tableReference4 = mockTableReference("keyspace2", "table4"); + OnDemandRepairJobView repairJobView4 = mockOnDemandRepairJobView(id, hostId, tableReference4, completedAt); + TableReference tableReference5 = mockTableReference("keyspace3", "table5"); + OnDemandRepairJobView repairJobView5 = mockOnDemandRepairJobView(id, hostId, tableReference5, completedAt); + Set tableReferencesForCluster = new HashSet<>(); + tableReferencesForCluster.add(tableReference1); + tableReferencesForCluster.add(tableReference2); + tableReferencesForCluster.add(tableReference3); + tableReferencesForCluster.add(tableReference4); + tableReferencesForCluster.add(tableReference5); + when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); + + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new OnDemandRepair(repairJobView1)); + expectedResponse.add(new OnDemandRepair(repairJobView2)); + expectedResponse.add(new OnDemandRepair(repairJobView3)); + expectedResponse.add(new OnDemandRepair(repairJobView4)); + expectedResponse.add(new OnDemandRepair(repairJobView5)); + + when(myOnDemandRepairScheduler.scheduleJob(tableReference1)).thenReturn(repairJobView1); + when(myOnDemandRepairScheduler.scheduleJob(tableReference2)).thenReturn(repairJobView2); + when(myOnDemandRepairScheduler.scheduleJob(tableReference3)).thenReturn(repairJobView3); + when(myOnDemandRepairScheduler.scheduleJob(tableReference4)).thenReturn(repairJobView4); + when(myOnDemandRepairScheduler.scheduleJob(tableReference5)).thenReturn(repairJobView5); + when(myReplicatedTableProvider.accept("keyspace1")).thenReturn(true); + when(myReplicatedTableProvider.accept("keyspace2")).thenReturn(true); + when(myReplicatedTableProvider.accept("keyspace3")).thenReturn(true); + ResponseEntity> response = OnDemandRest.runRepair(null, null, true); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + private OnDemandRepairJobView mockOnDemandRepairJobView(UUID id, UUID hostId, TableReference tableReference, + Long completedAt) + { + return new OnDemandRepairJobView(id, hostId, tableReference, OnDemandRepairJobView.Status.IN_QUEUE, 0.0, + completedAt); + } + + @Test + public void testRunRepairNoKeyspaceWithTable() + { + ResponseEntity> response = null; + try + { + response = OnDemandRest.runRepair(null, "table1", true); + } + catch (ResponseStatusException e) + { + assertThat(e.getRawStatusCode()).isEqualTo(BAD_REQUEST.value()); + } + assertThat(response).isNull(); + } + + public TableReference mockTableReference(String keyspace, String table) + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getKeyspace()).thenReturn(keyspace); + when(tableReference.getTable()).thenReturn(table); + when(tableReference.getGcGraceSeconds()).thenReturn(DEFAULT_GC_GRACE_SECONDS); + when(myTableReferenceFactory.forTable(eq(keyspace), eq(table))).thenReturn(tableReference); + return tableReference; + } +} diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java index d57af4ee9..4ae3c387d 100644 --- a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestRepairManagementRESTImpl.java @@ -15,25 +15,12 @@ package com.ericsson.bss.cassandra.ecchronos.rest; import com.ericsson.bss.cassandra.ecchronos.core.exceptions.EcChronosException; -import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairJobView; -import com.ericsson.bss.cassandra.ecchronos.core.repair.OnDemandRepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairConfiguration; -import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; -import com.ericsson.bss.cassandra.ecchronos.core.repair.ScheduledRepairJobView; -import com.ericsson.bss.cassandra.ecchronos.core.repair.state.RepairStateSnapshot; -import com.ericsson.bss.cassandra.ecchronos.core.repair.state.VnodeRepairState; -import com.ericsson.bss.cassandra.ecchronos.core.repair.state.VnodeRepairStates; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.OnDemandRepair; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairInfo; import com.ericsson.bss.cassandra.ecchronos.core.repair.types.RepairStats; -import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; -import com.ericsson.bss.cassandra.ecchronos.core.utils.DriverNode; -import com.ericsson.bss.cassandra.ecchronos.core.utils.LongTokenRange; import com.ericsson.bss.cassandra.ecchronos.core.utils.RepairStatsProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.ReplicatedTableProvider; import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReferenceFactory; -import com.google.common.collect.ImmutableSet; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,17 +30,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.server.ResponseStatusException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -61,17 +42,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.NOT_FOUND; @RunWith(MockitoJUnitRunner.Silent.class) public class TestRepairManagementRESTImpl { - private static final int DEFAULT_GC_GRACE_SECONDS = 7200; - @Mock - private RepairScheduler myRepairScheduler; - - @Mock - private OnDemandRepairScheduler myOnDemandRepairScheduler; + public static final int DEFAULT_GC_GRACE_SECONDS = 7200; @Mock private ReplicatedTableProvider myReplicatedTableProvider; @@ -82,432 +57,13 @@ public class TestRepairManagementRESTImpl @Mock private TableReferenceFactory myTableReferenceFactory; - private RepairManagementREST repairManagementREST; + private RepairManagementREST managementREST; @Before public void setupMocks() { - repairManagementREST = new RepairManagementRESTImpl(myRepairScheduler, myOnDemandRepairScheduler, - myTableReferenceFactory, myReplicatedTableProvider, myRepairStatsProvider); - } - - @Test - public void testGetNoRepairs() - { - when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(new ArrayList<>()); - - ResponseEntity> response; - - response = repairManagementREST.getRepairs(null, null, null); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - - response = repairManagementREST.getRepairs("ks", null, null); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - - response = repairManagementREST.getRepairs("ks", "tb", null); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - - response = repairManagementREST.getRepairs("ks", "tb", UUID.randomUUID().toString()); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - } - - @Test - public void testGetRepairs() - { - UUID expectedId = UUID.randomUUID(); - UUID expectedHostId = UUID.randomUUID(); - TableReference tableReference1 = mockTableReference("ks", "tb"); - OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, 1234L); - TableReference tableReference2 = mockTableReference("ks", "tb2"); - OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, - 2345L); - OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, - 3456L); - List repairJobViews = Arrays.asList(job1, job2, job3); - - List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) - .collect(Collectors.toList()); - - when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); - - ResponseEntity> response = repairManagementREST.getRepairs(null, null, null); - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs(null, null, expectedHostId.toString()); - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", null, null); - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", null, expectedHostId.toString()); - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", "tb", null); - assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", "tb", expectedHostId.toString()); - assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", "tb", UUID.randomUUID().toString()); - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("wrong", "tb", null); - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("ks", "wrong", null); - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getRepairs("wrong", "wrong", null); - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void testGetRepairWithId() - { - UUID expectedId = UUID.randomUUID(); - UUID expectedHostId = UUID.randomUUID(); - TableReference tableReference1 = mockTableReference("ks", "tb"); - OnDemandRepairJobView job1 = mockOnDemandRepairJobView(expectedId, expectedHostId, tableReference1, 1234L); - TableReference tableReference2 = mockTableReference("ks", "tb2"); - OnDemandRepairJobView job2 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, - 2345L); - OnDemandRepairJobView job3 = mockOnDemandRepairJobView(UUID.randomUUID(), expectedHostId, tableReference2, - 3456L); - List repairJobViews = Arrays.asList(job1, job2, job3); - - List expectedResponse = repairJobViews.stream().map(OnDemandRepair::new) - .collect(Collectors.toList()); - - when(myOnDemandRepairScheduler.getAllClusterWideRepairJobs()).thenReturn(repairJobViews); - ResponseEntity> response = null; - try - { - response = repairManagementREST.getRepairs(UUID.randomUUID().toString(), expectedHostId.toString()); - } - catch (ResponseStatusException e) - { - assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); - } - assertThat(response).isNull(); - response = repairManagementREST.getRepairs(expectedId.toString(), expectedHostId.toString()); - assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = null; - try - { - response = repairManagementREST.getRepairs(UUID.randomUUID().toString(), null); - } - catch (ResponseStatusException e) - { - assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); - } - assertThat(response).isNull(); - response = repairManagementREST.getRepairs(expectedId.toString(), null); - assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void testTriggerRepair() throws EcChronosException - { - UUID id = UUID.randomUUID(); - UUID hostId = UUID.randomUUID(); - long completedAt = 1234L; - TableReference tableReference = mockTableReference("ks", "tb"); - OnDemandRepairJobView localRepairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt); - List localExpectedResponse = Collections.singletonList(new OnDemandRepair(localRepairJobView)); - - when(myOnDemandRepairScheduler.scheduleJob(tableReference)).thenReturn(localRepairJobView); - when(myReplicatedTableProvider.accept("ks")).thenReturn(true); - ResponseEntity> response = repairManagementREST.triggerRepair("ks", "tb", true); - - assertThat(response.getBody()).isEqualTo(localExpectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - OnDemandRepairJobView repairJobView = mockOnDemandRepairJobView(id, hostId, tableReference, completedAt); - List expectedResponse = Collections.singletonList(new OnDemandRepair(repairJobView)); - - when(myOnDemandRepairScheduler.scheduleClusterWideJob(tableReference)).thenReturn( - Collections.singletonList(repairJobView)); - response = repairManagementREST.triggerRepair("ks", "tb", false); - - assertThat(response.getBody()).isEqualTo(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void testTriggerRepairOnlyKeyspace() throws EcChronosException - { - UUID id = UUID.randomUUID(); - UUID hostId = UUID.randomUUID(); - long completedAt = 1234L; - TableReference tableReference1 = mockTableReference("ks", "table1"); - OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt); - TableReference tableReference2 = mockTableReference("ks", "table2"); - OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt); - TableReference tableReference3 = mockTableReference("ks", "table3"); - OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt); - - Set tableReferencesForKeyspace = new HashSet<>(); - tableReferencesForKeyspace.add(tableReference1); - tableReferencesForKeyspace.add(tableReference2); - tableReferencesForKeyspace.add(tableReference3); - when(myTableReferenceFactory.forKeyspace("ks")).thenReturn(tableReferencesForKeyspace); - List expectedResponse = new ArrayList<>(); - expectedResponse.add(new OnDemandRepair(repairJobView1)); - expectedResponse.add(new OnDemandRepair(repairJobView2)); - expectedResponse.add(new OnDemandRepair(repairJobView3)); - - when(myOnDemandRepairScheduler.scheduleJob(tableReference1)).thenReturn(repairJobView1); - when(myOnDemandRepairScheduler.scheduleJob(tableReference2)).thenReturn(repairJobView2); - when(myOnDemandRepairScheduler.scheduleJob(tableReference3)).thenReturn(repairJobView3); - when(myReplicatedTableProvider.accept("ks")).thenReturn(true); - ResponseEntity> response = repairManagementREST.triggerRepair("ks", null, true); - - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void testTriggerRepairNoKeyspaceNoTable() throws EcChronosException - { - UUID id = UUID.randomUUID(); - UUID hostId = UUID.randomUUID(); - long completedAt = 1234L; - TableReference tableReference1 = mockTableReference("keyspace1", "table1"); - OnDemandRepairJobView repairJobView1 = mockOnDemandRepairJobView(id, hostId, tableReference1, completedAt); - TableReference tableReference2 = mockTableReference("keyspace1", "table2"); - OnDemandRepairJobView repairJobView2 = mockOnDemandRepairJobView(id, hostId, tableReference2, completedAt); - TableReference tableReference3 = mockTableReference("keyspace1", "table3"); - OnDemandRepairJobView repairJobView3 = mockOnDemandRepairJobView(id, hostId, tableReference3, completedAt); - TableReference tableReference4 = mockTableReference("keyspace2", "table4"); - OnDemandRepairJobView repairJobView4 = mockOnDemandRepairJobView(id, hostId, tableReference4, completedAt); - TableReference tableReference5 = mockTableReference("keyspace3", "table5"); - OnDemandRepairJobView repairJobView5 = mockOnDemandRepairJobView(id, hostId, tableReference5, completedAt); - Set tableReferencesForCluster = new HashSet<>(); - tableReferencesForCluster.add(tableReference1); - tableReferencesForCluster.add(tableReference2); - tableReferencesForCluster.add(tableReference3); - tableReferencesForCluster.add(tableReference4); - tableReferencesForCluster.add(tableReference5); - when(myTableReferenceFactory.forCluster()).thenReturn(tableReferencesForCluster); - - List expectedResponse = new ArrayList<>(); - expectedResponse.add(new OnDemandRepair(repairJobView1)); - expectedResponse.add(new OnDemandRepair(repairJobView2)); - expectedResponse.add(new OnDemandRepair(repairJobView3)); - expectedResponse.add(new OnDemandRepair(repairJobView4)); - expectedResponse.add(new OnDemandRepair(repairJobView5)); - - when(myOnDemandRepairScheduler.scheduleJob(tableReference1)).thenReturn(repairJobView1); - when(myOnDemandRepairScheduler.scheduleJob(tableReference2)).thenReturn(repairJobView2); - when(myOnDemandRepairScheduler.scheduleJob(tableReference3)).thenReturn(repairJobView3); - when(myOnDemandRepairScheduler.scheduleJob(tableReference4)).thenReturn(repairJobView4); - when(myOnDemandRepairScheduler.scheduleJob(tableReference5)).thenReturn(repairJobView5); - when(myReplicatedTableProvider.accept("keyspace1")).thenReturn(true); - when(myReplicatedTableProvider.accept("keyspace2")).thenReturn(true); - when(myReplicatedTableProvider.accept("keyspace3")).thenReturn(true); - ResponseEntity> response = repairManagementREST.triggerRepair(null, null, true); - - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - private OnDemandRepairJobView mockOnDemandRepairJobView(UUID id, UUID hostId, TableReference tableReference, - Long completedAt) - { - return new OnDemandRepairJobView(id, hostId, tableReference, OnDemandRepairJobView.Status.IN_QUEUE, 0.0, - completedAt); - } - - @Test - public void testTriggerRepairNoKeyspaceWithTable() - { - ResponseEntity> response = null; - try - { - response = repairManagementREST.triggerRepair(null, "table1", true); - } - catch (ResponseStatusException e) - { - assertThat(e.getRawStatusCode()).isEqualTo(BAD_REQUEST.value()); - } - assertThat(response).isNull(); - } - - @Test - public void testGetNoSchedules() - { - when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(new ArrayList<>()); - - ResponseEntity> response; - - response = repairManagementREST.getSchedules(null, null); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - - response = repairManagementREST.getSchedules("ks", null); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - - response = repairManagementREST.getSchedules("ks", "tb"); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - } - - @Test - public void testGetSchedules() throws UnknownHostException - { - DriverNode replica = mock(DriverNode.class); - when(replica.getPublicAddress()).thenReturn(InetAddress.getLocalHost()); - VnodeRepairState expectedVnodeRepairState1 = new VnodeRepairState(new LongTokenRange(2, 3), - ImmutableSet.of(replica), 1234L); - TableReference tableReference1 = mockTableReference("ks", "tb"); - ScheduledRepairJobView job1 = mockScheduledRepairJobView(tableReference1, UUID.randomUUID(), 1234L, 11L, - ImmutableSet.of(expectedVnodeRepairState1)); - VnodeRepairState expectedVnodeRepairState2 = new VnodeRepairState(new LongTokenRange(2, 3), - ImmutableSet.of(replica), 2345L); - TableReference tableReference2 = mockTableReference("ks", "tb2"); - ScheduledRepairJobView job2 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 2345L, 12L, - ImmutableSet.of(expectedVnodeRepairState2)); - VnodeRepairState expectedVnodeRepairState3 = new VnodeRepairState(new LongTokenRange(2, 3), - ImmutableSet.of(replica), 3333L); - ScheduledRepairJobView job3 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 3333L, 15L, - ImmutableSet.of(expectedVnodeRepairState3)); - List repairJobViews = Arrays.asList(job1, job2, job3); - - List expectedResponse = repairJobViews.stream().map(Schedule::new).collect(Collectors.toList()); - - when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(repairJobViews); - - ResponseEntity> response; - - response = repairManagementREST.getSchedules(null, null); - - assertThat(response.getBody()).isEqualTo(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getSchedules("ks", null); - - assertThat(response.getBody()).containsAll(expectedResponse); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getSchedules("ks", "tb"); - - assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getSchedules("wrong", "tb"); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getSchedules("ks", "wrong"); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - response = repairManagementREST.getSchedules("wrong", "wrong"); - - assertThat(response.getBody()).isEmpty(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - @Test - public void testGetScheduleWithId() throws UnknownHostException - { - UUID expectedId = UUID.randomUUID(); - long expectedLastRepairedAt = 234; - DriverNode replica = mock(DriverNode.class); - when(replica.getPublicAddress()).thenReturn(InetAddress.getLocalHost()); - VnodeRepairState expectedVnodeRepairState = new VnodeRepairState(new LongTokenRange(2, 3), - ImmutableSet.of(replica), expectedLastRepairedAt); - TableReference tableReference1 = mockTableReference("ks", "tb"); - ScheduledRepairJobView job1 = mockScheduledRepairJobView(tableReference1, expectedId, 1234L, 11L, - ImmutableSet.of(expectedVnodeRepairState)); - TableReference tableReference2 = mockTableReference("ks", "tb2"); - ScheduledRepairJobView job2 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 2345L, 12L, - ImmutableSet.of(expectedVnodeRepairState)); - ScheduledRepairJobView job3 = mockScheduledRepairJobView(tableReference2, expectedId, 2365L, 12L, - ImmutableSet.of(expectedVnodeRepairState)); - List repairJobViews = Arrays.asList(job1, job2, job3); - - List expectedResponse = repairJobViews.stream().map(view -> new Schedule(view, false)) - .collect(Collectors.toList()); - - when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(repairJobViews); - ResponseEntity response = null; - - try - { - response = repairManagementREST.getSchedules(UUID.randomUUID().toString(), false); - } - catch (ResponseStatusException e) - { - assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); - } - - assertThat(response).isNull(); - - response = repairManagementREST.getSchedules(expectedId.toString(), false); - - assertThat(response.getBody()).isEqualTo(expectedResponse.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - - List expectedResponseFull = repairJobViews.stream().map(view -> new Schedule(view, true)) - .collect(Collectors.toList()); - - response = null; - try - { - response = repairManagementREST.getSchedules(UUID.randomUUID().toString(), true); - } - catch (ResponseStatusException e) - { - assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); - } - assertThat(response).isNull(); - - response = repairManagementREST.getSchedules(expectedId.toString(), true); - - assertThat(response.getBody()).isEqualTo(expectedResponseFull.get(0)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().virtualNodeStates).isNotEmpty(); - } - - private ScheduledRepairJobView mockScheduledRepairJobView(TableReference tableReference, UUID id, - long lastRepairedAt, long repairInterval, Set vnodeRepairState) - { - RepairConfiguration repairConfigurationMock = mock(RepairConfiguration.class); - when(repairConfigurationMock.getRepairIntervalInMs()).thenReturn(repairInterval); - VnodeRepairStates vnodeRepairStatesMock = mock(VnodeRepairStates.class); - when(vnodeRepairStatesMock.getVnodeRepairStates()).thenReturn(vnodeRepairState); - RepairStateSnapshot repairStateSnapshotMock = mock(RepairStateSnapshot.class); - when(repairStateSnapshotMock.getVnodeRepairStates()).thenReturn(vnodeRepairStatesMock); - return new ScheduledRepairJobView(id, tableReference, repairConfigurationMock, repairStateSnapshotMock, - ScheduledRepairJobView.Status.ON_TIME, 0.0, lastRepairedAt + repairInterval); + managementREST = new RepairManagementRESTImpl(myTableReferenceFactory, myReplicatedTableProvider, + myRepairStatsProvider); } @Test @@ -549,7 +105,7 @@ public void testGetRepairInfo() repairStats.add(repairStats4); repairStats.add(repairStats5); RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); - ResponseEntity response = repairManagementREST.getRepairInfo(null, null, since, duration, true); + ResponseEntity response = managementREST.getRepairInfo(null, null, since, duration, true); RepairInfo returnedRepairInfo = response.getBody(); assertThat(returnedRepairInfo).isEqualTo(expectedResponse); @@ -597,7 +153,7 @@ public void testGetRepairInfoOnlySince() repairStats.add(repairStats4); repairStats.add(repairStats5); RepairInfo expectedResponse = new RepairInfo(since, 0L, repairStats); - ResponseEntity response = repairManagementREST.getRepairInfo(null, null, since, null, true); + ResponseEntity response = managementREST.getRepairInfo(null, null, since, null, true); RepairInfo returnedRepairInfo = response.getBody(); assertThat(returnedRepairInfo.repairStats).containsExactlyInAnyOrderElementsOf(expectedResponse.repairStats); @@ -647,7 +203,7 @@ public void testGetRepairInfoOnlyDuration() repairStats.add(repairStats3); repairStats.add(repairStats4); repairStats.add(repairStats5); - ResponseEntity response = repairManagementREST.getRepairInfo(null, null, null, duration, true); + ResponseEntity response = managementREST.getRepairInfo(null, null, null, duration, true); RepairInfo returnedRepairInfo = response.getBody(); assertThat(returnedRepairInfo.repairStats).containsExactlyInAnyOrderElementsOf(repairStats); @@ -683,7 +239,7 @@ public void testGetRepairInfoOnlyKeyspace() throws EcChronosException repairStats.add(repairStats2); repairStats.add(repairStats3); RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); - ResponseEntity response = repairManagementREST.getRepairInfo("keyspace1", null, since, duration, + ResponseEntity response = managementREST.getRepairInfo("keyspace1", null, since, duration, true); RepairInfo returnedRepairInfo = response.getBody(); @@ -705,7 +261,7 @@ public void testGetRepairInfoKeyspaceAndTable() List repairStats = new ArrayList<>(); repairStats.add(repairStats1); RepairInfo expectedResponse = new RepairInfo(since, to, repairStats); - ResponseEntity response = repairManagementREST.getRepairInfo("keyspace1", "table1", since, duration, + ResponseEntity response = managementREST.getRepairInfo("keyspace1", "table1", since, duration, true); RepairInfo returnedRepairInfo = response.getBody(); @@ -722,7 +278,7 @@ public void testGetRepairInfoOnlyTable() ResponseEntity response = null; try { - response = repairManagementREST.getRepairInfo(null, "table1", since, duration, true); + response = managementREST.getRepairInfo(null, "table1", since, duration, true); } catch (ResponseStatusException e) { @@ -741,7 +297,7 @@ public void testGetRepairInfoForKeyspaceAndTableNoSinceOrDuration() when(myReplicatedTableProvider.accept("keyspace1")).thenReturn(true); List repairStats = new ArrayList<>(); repairStats.add(repairStats1); - ResponseEntity response = repairManagementREST.getRepairInfo("keyspace1", "table1", null, null, + ResponseEntity response = managementREST.getRepairInfo("keyspace1", "table1", null, null, true); RepairInfo returnedRepairInfo = response.getBody(); @@ -757,7 +313,7 @@ public void testGetRepairInfoNoKeyspaceNoTableNoSinceOrDuration() ResponseEntity response = null; try { - response = repairManagementREST.getRepairInfo(null, null, null, null, true); + response = managementREST.getRepairInfo(null, null, null, null, true); } catch (ResponseStatusException e) { @@ -773,7 +329,7 @@ public void testGetRepairInfoSinceBiggerThanSincePlusDuration() ResponseEntity response = null; try { - response = repairManagementREST.getRepairInfo("keyspace1", "table1", 0L, Duration.ofMillis(-1000), true); + response = managementREST.getRepairInfo("keyspace1", "table1", 0L, Duration.ofMillis(-1000), true); } catch (ResponseStatusException e) { @@ -782,7 +338,7 @@ public void testGetRepairInfoSinceBiggerThanSincePlusDuration() assertThat(response).isNull(); } - private TableReference mockTableReference(String keyspace, String table) + public TableReference mockTableReference(String keyspace, String table) { TableReference tableReference = mock(TableReference.class); when(tableReference.getKeyspace()).thenReturn(keyspace); diff --git a/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestScheduleRepairManagementRESTImpl.java b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestScheduleRepairManagementRESTImpl.java new file mode 100644 index 000000000..400788d5b --- /dev/null +++ b/rest/src/test/java/com/ericsson/bss/cassandra/ecchronos/rest/TestScheduleRepairManagementRESTImpl.java @@ -0,0 +1,229 @@ +/* + * Copyright 2023 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecchronos.rest; + +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairConfiguration; +import com.ericsson.bss.cassandra.ecchronos.core.repair.RepairScheduler; +import com.ericsson.bss.cassandra.ecchronos.core.repair.ScheduledRepairJobView; +import com.ericsson.bss.cassandra.ecchronos.core.repair.state.RepairStateSnapshot; +import com.ericsson.bss.cassandra.ecchronos.core.repair.state.VnodeRepairState; +import com.ericsson.bss.cassandra.ecchronos.core.repair.state.VnodeRepairStates; +import com.ericsson.bss.cassandra.ecchronos.core.repair.types.Schedule; +import com.ericsson.bss.cassandra.ecchronos.core.utils.DriverNode; +import com.ericsson.bss.cassandra.ecchronos.core.utils.LongTokenRange; +import com.ericsson.bss.cassandra.ecchronos.core.utils.TableReference; +import com.google.common.collect.ImmutableSet; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class TestScheduleRepairManagementRESTImpl +{ + @Mock + private RepairScheduler myRepairScheduler; + + + private ScheduleRepairManagementREST ScheduleREST; + + @Before + public void setupMocks() + { + ScheduleREST = new ScheduleRepairManagementRESTImpl(myRepairScheduler); + } + + @Test + public void testGetNoSchedules() + { + when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(new ArrayList<>()); + + ResponseEntity> response; + + response = ScheduleREST.getSchedules(null, null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = ScheduleREST.getSchedules("ks", null); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + + response = ScheduleREST.getSchedules("ks", "tb"); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void testGetSchedules() throws UnknownHostException + { + DriverNode replica = mock(DriverNode.class); + when(replica.getPublicAddress()).thenReturn(InetAddress.getLocalHost()); + VnodeRepairState expectedVnodeRepairState1 = new VnodeRepairState(new LongTokenRange(2, 3), + ImmutableSet.of(replica), 1234L); + TableReference tableReference1 = mockTableReference("ks", "tb"); + ScheduledRepairJobView job1 = mockScheduledRepairJobView(tableReference1, UUID.randomUUID(), 1234L, 11L, + ImmutableSet.of(expectedVnodeRepairState1)); + VnodeRepairState expectedVnodeRepairState2 = new VnodeRepairState(new LongTokenRange(2, 3), + ImmutableSet.of(replica), 2345L); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + ScheduledRepairJobView job2 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 2345L, 12L, + ImmutableSet.of(expectedVnodeRepairState2)); + VnodeRepairState expectedVnodeRepairState3 = new VnodeRepairState(new LongTokenRange(2, 3), + ImmutableSet.of(replica), 3333L); + ScheduledRepairJobView job3 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 3333L, 15L, + ImmutableSet.of(expectedVnodeRepairState3)); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(Schedule::new).collect(Collectors.toList()); + + when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(repairJobViews); + + ResponseEntity> response; + + response = ScheduleREST.getSchedules(null, null); + + assertThat(response.getBody()).isEqualTo(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = ScheduleREST.getSchedules("ks", null); + + assertThat(response.getBody()).containsAll(expectedResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = ScheduleREST.getSchedules("ks", "tb"); + + assertThat(response.getBody()).containsExactly(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = ScheduleREST.getSchedules("wrong", "tb"); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = ScheduleREST.getSchedules("ks", "wrong"); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + response = ScheduleREST.getSchedules("wrong", "wrong"); + + assertThat(response.getBody()).isEmpty(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void testGetScheduleWithId() throws UnknownHostException + { + UUID expectedId = UUID.randomUUID(); + long expectedLastRepairedAt = 234; + DriverNode replica = mock(DriverNode.class); + when(replica.getPublicAddress()).thenReturn(InetAddress.getLocalHost()); + VnodeRepairState expectedVnodeRepairState = new VnodeRepairState(new LongTokenRange(2, 3), + ImmutableSet.of(replica), expectedLastRepairedAt); + TableReference tableReference1 = mockTableReference("ks", "tb"); + ScheduledRepairJobView job1 = mockScheduledRepairJobView(tableReference1, expectedId, 1234L, 11L, + ImmutableSet.of(expectedVnodeRepairState)); + TableReference tableReference2 = mockTableReference("ks", "tb2"); + ScheduledRepairJobView job2 = mockScheduledRepairJobView(tableReference2, UUID.randomUUID(), 2345L, 12L, + ImmutableSet.of(expectedVnodeRepairState)); + ScheduledRepairJobView job3 = mockScheduledRepairJobView(tableReference2, expectedId, 2365L, 12L, + ImmutableSet.of(expectedVnodeRepairState)); + List repairJobViews = Arrays.asList(job1, job2, job3); + + List expectedResponse = repairJobViews.stream().map(view -> new Schedule(view, false)) + .collect(Collectors.toList()); + + when(myRepairScheduler.getCurrentRepairJobs()).thenReturn(repairJobViews); + ResponseEntity response = null; + + try + { + response = ScheduleREST.getSchedules(UUID.randomUUID().toString(), false); + } + catch (ResponseStatusException e) + { + assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); + } + + assertThat(response).isNull(); + + response = ScheduleREST.getSchedules(expectedId.toString(), false); + + assertThat(response.getBody()).isEqualTo(expectedResponse.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + List expectedResponseFull = repairJobViews.stream().map(view -> new Schedule(view, true)) + .collect(Collectors.toList()); + + response = null; + try + { + response = ScheduleREST.getSchedules(UUID.randomUUID().toString(), true); + } + catch (ResponseStatusException e) + { + assertThat(e.getRawStatusCode()).isEqualTo(NOT_FOUND.value()); + } + assertThat(response).isNull(); + + response = ScheduleREST.getSchedules(expectedId.toString(), true); + + assertThat(response.getBody()).isEqualTo(expectedResponseFull.get(0)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody().virtualNodeStates).isNotEmpty(); + } + + private ScheduledRepairJobView mockScheduledRepairJobView(TableReference tableReference, UUID id, + long lastRepairedAt, long repairInterval, Set vnodeRepairState) + { + RepairConfiguration repairConfigurationMock = mock(RepairConfiguration.class); + when(repairConfigurationMock.getRepairIntervalInMs()).thenReturn(repairInterval); + VnodeRepairStates vnodeRepairStatesMock = mock(VnodeRepairStates.class); + when(vnodeRepairStatesMock.getVnodeRepairStates()).thenReturn(vnodeRepairState); + RepairStateSnapshot repairStateSnapshotMock = mock(RepairStateSnapshot.class); + when(repairStateSnapshotMock.getVnodeRepairStates()).thenReturn(vnodeRepairStatesMock); + return new ScheduledRepairJobView(id, tableReference, repairConfigurationMock, repairStateSnapshotMock, + ScheduledRepairJobView.Status.ON_TIME, 0.0, lastRepairedAt + repairInterval); + } + + public TableReference mockTableReference(String keyspace, String table) + { + TableReference tableReference = mock(TableReference.class); + when(tableReference.getKeyspace()).thenReturn(keyspace); + when(tableReference.getTable()).thenReturn(table); + return tableReference; + } +}