diff --git a/genie-web/src/docs/asciidoc/api/commands/_api.adoc b/genie-web/src/docs/asciidoc/api/commands/_api.adoc index 918cd6d08b2..7916dd166f1 100644 --- a/genie-web/src/docs/asciidoc/api/commands/_api.adoc +++ b/genie-web/src/docs/asciidoc/api/commands/_api.adoc @@ -32,3 +32,4 @@ include::_addClusterCriterionForCommand.adoc[] include::_setClusterCriteriaForCommand.adoc[] include::_addClusterCriterionWithPriorityForCommand.adoc[] include::_removeClusterCriterionFromCommand.adoc[] +include::_resolveClustersForCommandClusterCriterion.adoc[] diff --git a/genie-web/src/docs/asciidoc/api/commands/_getClustersForCommand.adoc b/genie-web/src/docs/asciidoc/api/commands/_getClustersForCommand.adoc index 96ccf2cfbc5..a80242b7f05 100644 --- a/genie-web/src/docs/asciidoc/api/commands/_getClustersForCommand.adoc +++ b/genie-web/src/docs/asciidoc/api/commands/_getClustersForCommand.adoc @@ -6,7 +6,7 @@ Search the clusters that the given command is linked to. ==== Endpoint -`GET /api/v3/applications/{id}/commands` +`GET /api/v3/commands/{id}/clusters` :snippet-base: {snippets}/command-rest-controller-integration-test/can-get-clusters-for-command/8 :id-base: get-clusters-for-command diff --git a/genie-web/src/docs/asciidoc/api/commands/_resolveClustersForCommandClusterCriterion.adoc b/genie-web/src/docs/asciidoc/api/commands/_resolveClustersForCommandClusterCriterion.adoc new file mode 100644 index 00000000000..0da1ec583dd --- /dev/null +++ b/genie-web/src/docs/asciidoc/api/commands/_resolveClustersForCommandClusterCriterion.adoc @@ -0,0 +1,26 @@ +=== Resolve Clusters for Command Criterion + +==== Description + +For a given `Command` retrieve the `Criterion` identified by `priority` and attempt to +resolve all the `Cluster's` this `Criterion` would currently match within the database. + +==== Endpoint + +`GET /api/v3/commands/{id}/clusterCriteria/{priority}` + +:snippet-base: {snippets}/command-rest-controller-integration-test/test-resolve-clusters-for-command-cluster-criterion/5 +:id-base: resolve-clusters-for-command-cluster-criterion +:!request-headers: +:request-path-params: {snippet-base}/path-parameters.adoc +:request-query-params: {snippet-base}/request-parameters.adoc +:!request-fields: +:curl-request: {snippet-base}/curl-request.adoc +:httpie-request: {snippet-base}/httpie-request.adoc +:response-headers: {snippet-base}/response-headers.adoc +:response-fields: {snippet-base}/response-fields.adoc +:!response-links: +:http-request: {snippet-base}/http-request.adoc +:http-response: {snippet-base}/http-response.adoc + +include::../_apiTemplate.adoc[] diff --git a/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestControllerIntegrationTest.java b/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestControllerIntegrationTest.java index 0c7d893be3e..65ef102ebb9 100644 --- a/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestControllerIntegrationTest.java +++ b/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestControllerIntegrationTest.java @@ -1989,6 +1989,138 @@ public void testRemoveClusterCriterionFromCommand() throws Exception { .statusCode(HttpStatus.NOT_FOUND.value()); } + /** + * Test to make sure we can resolve clusters for a command given a criterion. + * + * @throws Exception on unexpected error + */ + @Test + public void testResolveClustersForCommandClusterCriterion() throws Exception { + final Cluster cluster0 = new Cluster.Builder( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + ClusterStatus.UP + ) + .withTags(Sets.newHashSet(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .build(); + final Cluster cluster1 = new Cluster.Builder( + cluster0.getName(), + cluster0.getUser(), + cluster0.getVersion(), + ClusterStatus.OUT_OF_SERVICE + ) + .withTags(cluster0.getTags()) + .build(); + final Cluster cluster2 = new Cluster.Builder( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + ClusterStatus.UP + ) + .withTags(Sets.newHashSet(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .build(); + final String cluster0Id = this.createConfigResource(cluster0, null); + final String cluster1Id = this.createConfigResource(cluster1, null); + final String cluster2Id = this.createConfigResource(cluster2, null); + + final Command command = new Command + .Builder(NAME, USER, VERSION, CommandStatus.ACTIVE, EXECUTABLE_AND_ARGS, CHECK_DELAY) + .withClusterCriteria( + Lists.newArrayList( + new Criterion.Builder().withId(cluster0Id).build(), + new Criterion.Builder().withName(cluster1.getName()).build(), + new Criterion.Builder().withVersion(cluster2.getVersion()).build(), + new Criterion.Builder().withStatus(ClusterStatus.TERMINATED.name()).build(), + new Criterion.Builder().withTags(cluster2.getTags()).build() + ) + ) + .build(); + final String commandId = this.createConfigResource(command, null); + + final RestDocumentationFilter resolveFilter = RestAssuredRestDocumentation.document( + "{class-name}/{method-name}/{step}/", + // Path parameters + Snippets + .ID_PATH_PARAM + .and( + RequestDocumentation + .parameterWithName("priority") + .description("Priority of the criterion to insert") + ), + // Request parameters + RequestDocumentation.requestParameters( + RequestDocumentation + .parameterWithName("addDefaultStatus") + .description( + "Whether the system should add the default cluster status to the criterion. Default: true" + ) + .optional() + ), + // Response Content Type + Snippets.HAL_CONTENT_TYPE_HEADER, + // Response Fields + Snippets.resolveClustersForCommandClusterCriterionResponsePayload() + ); + + List clusterIds = RestAssured + .given(this.getRequestSpecification()) + .filter(resolveFilter) + .param("addDefaultStatus", "false") + .when() + .port(this.port) + .get(COMMANDS_API + "/{id}/clusterCriteria/{priority}/clusters", commandId, 1) + .then() + .statusCode(HttpStatus.OK.value()) + .contentType(Matchers.containsString(MediaTypes.HAL_JSON_VALUE)) + .extract() + .jsonPath() + .getList("id", String.class); + + Assertions.assertThat(clusterIds).hasSize(2).containsExactlyInAnyOrder(cluster0Id, cluster1Id); + + clusterIds = RestAssured + .given(this.getRequestSpecification()) + .filter(resolveFilter) + .when() + .port(this.port) + .get(COMMANDS_API + "/{id}/clusterCriteria/{priority}/clusters", commandId, 1) + .then() + .statusCode(HttpStatus.OK.value()) + .contentType(Matchers.containsString(MediaTypes.HAL_JSON_VALUE)) + .extract() + .jsonPath() + .getList("id", String.class); + + Assertions.assertThat(clusterIds).hasSize(1).containsExactlyInAnyOrder(cluster0Id); + + clusterIds = RestAssured + .given(this.getRequestSpecification()) + .when() + .port(this.port) + .get(COMMANDS_API + "/{id}/clusterCriteria/{priority}/clusters", commandId, 3) + .then() + .statusCode(HttpStatus.OK.value()) + .contentType(Matchers.containsString(MediaTypes.HAL_JSON_VALUE)) + .extract() + .jsonPath() + .getList("id", String.class); + Assertions.assertThat(clusterIds).isEmpty(); + + clusterIds = RestAssured + .given(this.getRequestSpecification()) + .when() + .port(this.port) + .get(COMMANDS_API + "/{id}/clusterCriteria/{priority}/clusters", commandId, 4) + .then() + .statusCode(HttpStatus.OK.value()) + .contentType(Matchers.containsString(MediaTypes.HAL_JSON_VALUE)) + .extract() + .jsonPath() + .getList("id", String.class); + Assertions.assertThat(clusterIds).containsExactlyInAnyOrder(cluster2Id); + } + private String createCommandWithDefaultClusterCriteria() throws Exception { return this.createConfigResource( new Command.Builder(NAME, USER, VERSION, CommandStatus.ACTIVE, EXECUTABLE_AND_ARGS, CHECK_DELAY) diff --git a/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/Snippets.java b/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/Snippets.java index a04474a6d0e..6aaa3e3898b 100644 --- a/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/Snippets.java +++ b/genie-web/src/integTest/java/com/netflix/genie/web/apis/rest/v3/controllers/Snippets.java @@ -717,6 +717,33 @@ static ResponseFieldsSnippet getClusterCriteriaForCommandResponsePayload() { .andWithPrefix("[].", getCriterionFieldDescriptors()); } + static ResponseFieldsSnippet resolveClustersForCommandClusterCriterionResponsePayload() { + return PayloadDocumentation + .responseFields( + PayloadDocumentation + .fieldWithPath("[]") + .attributes(EMPTY_CONSTRAINTS) + .description("The set of resolved clusters") + .type(JsonFieldType.ARRAY) + .optional() + ) + .andWithPrefix( + "[].", + ArrayUtils.add( + getClusterFieldDescriptors(), + PayloadDocumentation + .subsectionWithPath("links") + .attributes( + Attributes + .key(CONSTRAINTS) + .value("") + ) + .description("<<_hateoas,Links>> to other resources.") + .ignored() + ) + ); + } + private static FieldDescriptor[] getApplicationFieldDescriptors() { return ArrayUtils.addAll( getConfigFieldDescriptors(APPLICATION_CONSTRAINTS), diff --git a/genie-web/src/main/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestController.java b/genie-web/src/main/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestController.java index a899cc6fb97..f4aee483427 100644 --- a/genie-web/src/main/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestController.java +++ b/genie-web/src/main/java/com/netflix/genie/web/apis/rest/v3/controllers/CommandRestController.java @@ -36,6 +36,7 @@ import com.netflix.genie.web.apis.rest.v3.hateoas.assemblers.ClusterModelAssembler; import com.netflix.genie.web.apis.rest.v3.hateoas.assemblers.CommandModelAssembler; import com.netflix.genie.web.apis.rest.v3.hateoas.assemblers.EntityModelAssemblers; +import com.netflix.genie.web.data.services.ClusterPersistenceService; import com.netflix.genie.web.data.services.CommandPersistenceService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -90,6 +91,7 @@ public class CommandRestController { private final CommandPersistenceService commandPersistenceService; + private final ClusterPersistenceService clusterPersistenceService; private final CommandModelAssembler commandModelAssembler; private final ApplicationModelAssembler applicationModelAssembler; private final ClusterModelAssembler clusterModelAssembler; @@ -98,14 +100,17 @@ public class CommandRestController { * Constructor. * * @param commandPersistenceService The command configuration service to use. + * @param clusterPersistenceService The {@link ClusterPersistenceService} implementation to use * @param entityModelAssemblers The encapsulation of all available V3 resource assemblers */ @Autowired public CommandRestController( final CommandPersistenceService commandPersistenceService, + final ClusterPersistenceService clusterPersistenceService, final EntityModelAssemblers entityModelAssemblers ) { this.commandPersistenceService = commandPersistenceService; + this.clusterPersistenceService = clusterPersistenceService; this.commandModelAssembler = entityModelAssemblers.getCommandModelAssembler(); this.applicationModelAssembler = entityModelAssemblers.getApplicationModelAssembler(); this.clusterModelAssembler = entityModelAssemblers.getClusterModelAssembler(); @@ -788,4 +793,42 @@ public void removeClusterCriterionFromCommand( log.info("Called to remove the criterion from command {} with priority {}", id, priority); this.commandPersistenceService.removeClusterCriterionForCommand(id, priority); } + + /** + * For a given {@link Command} retrieve the {@link Criterion} identified by {@literal priority} and attempt to + * resolve all the {@link Cluster}'s this {@link Criterion} would currently match within the database. + * + * @param id The id of the command to get the criterion from + * @param priority The priority ({@literal 0} indexed). Must be + * {@literal 0 <= priority < clusterCriteria.length} + * @param addDefaultStatus Whether the system should add the default {@link ClusterStatus} to the {@link Criterion} + * if no status currently is within the {@link Criterion}. The default value is + * {@literal true} which will currently add the status {@link ClusterStatus#UP} + * @return The set of {@link Cluster}'s which matched the {@link Criterion} if any. Each {@link Cluster} is + * represented as an {@link EntityModel} with HAL links + * @throws GenieNotFoundException If no command with {@literal id} is found or if no {Criterion} with + * {@literal priority} exists within the command + */ + @GetMapping(value = "/{id}/clusterCriteria/{priority}/clusters", produces = MediaTypes.HAL_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + public Set> resolveClustersForCommandClusterCriterion( + @PathVariable("id") final String id, + @PathVariable("priority") @Min(0) final int priority, + @RequestParam(value = "addDefaultStatus", defaultValue = "true") final Boolean addDefaultStatus + ) throws GenieNotFoundException { + log.info("Called to get clusters for command {} cluster criterion {}", id, priority); + + // TODO: Improvement here to add API which only retrieves the single criterion but in the end the DB + // query likely will fetch entire collection without a lot of work and may not be worth the hassle + final List criteria = this.commandPersistenceService.getClusterCriteriaForCommand(id); + if (priority >= criteria.size()) { + throw new GenieNotFoundException("No criterion with priority " + priority + " exists for command " + id); + } + + return this.clusterPersistenceService.findClustersMatchingCriterion(criteria.get(priority), addDefaultStatus) + .stream() + .map(DtoConverters::toV3Cluster) + .map(this.clusterModelAssembler::toModel) + .collect(Collectors.toSet()); + } }