From df26dc012a326999578e4fca3d8430ae1bc0ff5d Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Tue, 19 Apr 2016 16:31:19 +0200 Subject: [PATCH] SONAR-7553 WS api/ce/activity_status display background tasks related metrics for UI --- .../server/ce/ws/ActivityStatusAction.java | 127 ++++++++++++ .../org/sonar/server/ce/ws/CeWsModule.java | 1 + .../server/ce/ws/activity_status-example.json | 4 + .../ce/ws/ActivityStatusActionTest.java | 191 ++++++++++++++++++ .../sonar/server/ce/ws/CeWsModuleTest.java | 3 +- .../ws/client/ce/ActivityStatusWsRequest.java | 71 +++++++ .../org/sonarqube/ws/client/ce/CeService.java | 10 + .../ws/client/ce/CeWsParameters.java | 1 + sonar-ws/src/main/protobuf/ws-ce.proto | 6 + 9 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/ce/ws/ActivityStatusAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/ce/ws/activity_status-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/ce/ws/ActivityStatusActionTest.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/ce/ActivityStatusWsRequest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/ce/ws/ActivityStatusAction.java b/server/sonar-server/src/main/java/org/sonar/server/ce/ws/ActivityStatusAction.java new file mode 100644 index 00000000000..b8e09db8b56 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ce/ws/ActivityStatusAction.java @@ -0,0 +1,127 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.ce.ws; + +import com.google.common.base.Optional; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.core.util.Uuids; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.ce.CeActivityDto; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.KeyExamples; +import org.sonarqube.ws.WsCe.ActivityStatusWsResponse; +import org.sonarqube.ws.client.ce.ActivityStatusWsRequest; + +import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_KEY; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_KEY; + +public class ActivityStatusAction implements CeWsAction { + private final UserSession userSession; + private final DbClient dbClient; + private final ComponentFinder componentFinder; + + public ActivityStatusAction(UserSession userSession, DbClient dbClient, ComponentFinder componentFinder) { + this.userSession = userSession; + this.dbClient = dbClient; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller + .createAction("activity_status") + .setDescription("Return CE activity related metrics.
" + + "Requires 'Administer System' permission or 'Administer' rights on the specified project.") + .setSince("5.5") + .setResponseExample(getClass().getResource("activity_status-example.json")) + .setInternal(true) + .setHandler(this); + + action.createParam(PARAM_COMPONENT_ID) + .setDescription("Id of the component (project) to filter on") + .setExampleValue(Uuids.UUID_EXAMPLE_03); + action.createParam(PARAM_COMPONENT_KEY) + .setDescription("Key of the component (project) to filter on") + .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001); + } + + @Override + public void handle(Request request, Response response) throws Exception { + ActivityStatusWsResponse activityStatusResponse = doHandle(toWsRequest(request)); + writeProtobuf(activityStatusResponse, request, response); + } + + private ActivityStatusWsResponse doHandle(ActivityStatusWsRequest request) { + DbSession dbSession = dbClient.openSession(false); + try { + Optional component = searchComponent(dbSession, request); + String componentUuid = component.isPresent() ? component.get().uuid() : null; + checkPermissions(componentUuid); + int pendingCount = dbClient.ceQueueDao().countByStatusAndComponentUuid(dbSession, CeQueueDto.Status.PENDING, componentUuid); + int failingCount = dbClient.ceActivityDao().countLastByStatusAndComponentUuid(dbSession, CeActivityDto.Status.FAILED, componentUuid); + + return ActivityStatusWsResponse.newBuilder() + .setPending(pendingCount) + .setFailing(failingCount) + .build(); + } finally { + dbClient.closeSession(dbSession); + } + } + + private Optional searchComponent(DbSession dbSession, ActivityStatusWsRequest request) { + ComponentDto component = null; + if (hasComponentInRequest(request)) { + component = componentFinder.getByUuidOrKey(dbSession, request.getComponentId(), request.getComponentKey(), COMPONENT_ID_AND_KEY); + } + return Optional.fromNullable(component); + } + + private void checkPermissions(@Nullable String componentUuid) { + if (componentUuid == null) { + userSession.checkPermission(GlobalPermissions.SYSTEM_ADMIN); + } else { + userSession.checkComponentUuidPermission(UserRole.ADMIN, componentUuid); + } + } + + private static boolean hasComponentInRequest(ActivityStatusWsRequest request) { + return request.getComponentId() != null || request.getComponentKey() != null; + } + + private static ActivityStatusWsRequest toWsRequest(Request request) { + return ActivityStatusWsRequest.newBuilder() + .setComponentId(request.param(PARAM_COMPONENT_ID)) + .setComponentKey(request.param(PARAM_COMPONENT_KEY)) + .build(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/ce/ws/CeWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/ce/ws/CeWsModule.java index 73b04cf18ba..ac527405405 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ce/ws/CeWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ce/ws/CeWsModule.java @@ -27,6 +27,7 @@ public class CeWsModule extends Module { add( CeWs.class, ActivityAction.class, + ActivityStatusAction.class, CancelAction.class, CancelAllAction.class, IsQueueEmptyWs.class, diff --git a/server/sonar-server/src/main/resources/org/sonar/server/ce/ws/activity_status-example.json b/server/sonar-server/src/main/resources/org/sonar/server/ce/ws/activity_status-example.json new file mode 100644 index 00000000000..c57d76edd38 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/ce/ws/activity_status-example.json @@ -0,0 +1,4 @@ +{ + "pending": 2, + "failing": 5 +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/ce/ws/ActivityStatusActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ce/ws/ActivityStatusActionTest.java new file mode 100644 index 00000000000..ad438ce56a4 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/ce/ws/ActivityStatusActionTest.java @@ -0,0 +1,191 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.ce.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.core.util.Uuids; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.ce.CeActivityDto; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsCe; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.ce.CeQueueTesting.newCeQueueDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_KEY; + +public class ActivityStatusActionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(db); + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + + WsActionTester ws = new WsActionTester(new ActivityStatusAction(userSession, dbClient, new ComponentFinder(dbClient))); + + @Before + public void setUp() { + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + } + + @Test + public void json_example() { + dbClient.ceQueueDao().insert(dbSession, newCeQueueDto("ce-queue-uuid-1").setStatus(CeQueueDto.Status.PENDING)); + dbClient.ceQueueDao().insert(dbSession, newCeQueueDto("ce-queue-uuid-2").setStatus(CeQueueDto.Status.PENDING)); + for (int i = 0; i < 5; i++) { + dbClient.ceActivityDao().insert(dbSession, new CeActivityDto(newCeQueueDto("ce-activity-uuid-" + i)) + .setStatus(CeActivityDto.Status.FAILED)); + } + db.commit(); + + String result = ws.newRequest().execute().getInput(); + + assertJson(result).isSimilarTo(getClass().getResource("activity_status-example.json")); + } + + @Test + public void status_for_a_project_as_project_admin() { + String projectUuid = "project-uuid"; + String anotherProjectUuid = "another-project-uuid"; + userSession.login().addProjectUuidPermissions(UserRole.ADMIN, projectUuid); + componentDb.insertComponent(newProjectDto(projectUuid)); + componentDb.insertComponent(newProjectDto(anotherProjectUuid)); + // pending tasks returned + insertInQueue(CeQueueDto.Status.PENDING, projectUuid); + insertInQueue(CeQueueDto.Status.PENDING, projectUuid); + // other tasks not returned + insertInQueue(CeQueueDto.Status.IN_PROGRESS, projectUuid); + insertInQueue(CeQueueDto.Status.PENDING, anotherProjectUuid); + insertInQueue(CeQueueDto.Status.PENDING, null); + // only one last activity for a given project + insertActivity(CeActivityDto.Status.SUCCESS, projectUuid); + insertActivity(CeActivityDto.Status.CANCELED, projectUuid); + insertActivity(CeActivityDto.Status.FAILED, projectUuid); + insertActivity(CeActivityDto.Status.FAILED, projectUuid); + insertActivity(CeActivityDto.Status.FAILED, anotherProjectUuid); + + WsCe.ActivityStatusWsResponse result = call(projectUuid); + + assertThat(result.getPending()).isEqualTo(2); + assertThat(result.getFailing()).isEqualTo(1); + } + + @Test + public void empty_status() { + WsCe.ActivityStatusWsResponse result = call(); + + assertThat(result.getPending()).isEqualTo(0); + assertThat(result.getFailing()).isEqualTo(0); + } + + @Test + public void fail_if_component_uuid_and_key_are_provided() { + ComponentDto project = newProjectDto(); + componentDb.insertComponent(project); + expectedException.expect(IllegalArgumentException.class); + + callByComponentUuidOrComponentKey(project.uuid(), project.key()); + } + + @Test + public void fail_if_component_uuid_is_unknown() { + expectedException.expect(NotFoundException.class); + + call("unknown-uuid"); + } + + @Test + public void fail_if_component_key_is_unknown() { + expectedException.expect(NotFoundException.class); + + callByComponentKey("unknown-key"); + } + + private void insertInQueue(CeQueueDto.Status status, @Nullable String componentUuid) { + dbClient.ceQueueDao().insert(dbSession, newCeQueueDto(Uuids.createFast()) + .setStatus(status) + .setComponentUuid(componentUuid)); + db.commit(); + } + + private void insertActivity(CeActivityDto.Status status, @Nullable String componentUuid) { + dbClient.ceActivityDao().insert(dbSession, new CeActivityDto( + newCeQueueDto(Uuids.createFast()) + .setComponentUuid(componentUuid)) + .setStatus(status)); + db.commit(); + } + + private WsCe.ActivityStatusWsResponse call() { + return callByComponentUuidOrComponentKey(null, null); + } + + private WsCe.ActivityStatusWsResponse call(String componentUuid) { + return callByComponentUuidOrComponentKey(componentUuid, null); + } + + private WsCe.ActivityStatusWsResponse callByComponentKey(String componentKey) { + return callByComponentUuidOrComponentKey(null, componentKey); + } + + private WsCe.ActivityStatusWsResponse callByComponentUuidOrComponentKey(@Nullable String componentUuid, @Nullable String componentKey) { + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF); + if (componentUuid != null) { + request.setParam(PARAM_COMPONENT_ID, componentUuid); + } + if (componentKey != null) { + request.setParam(PARAM_COMPONENT_KEY, componentKey); + } + + try { + return WsCe.ActivityStatusWsResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java index 75eb84a98e0..ddcd9422a37 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java @@ -21,7 +21,6 @@ package org.sonar.server.ce.ws; import org.junit.Test; import org.sonar.core.platform.ComponentContainer; -import org.sonar.server.ce.ws.CeWsModule; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +30,6 @@ public class CeWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new CeWsModule().configure(container); - assertThat(container.size()).isEqualTo(11 + 2 /* injected by ComponentContainer */); + assertThat(container.size()).isEqualTo(12 + 2 /* injected by ComponentContainer */); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/ActivityStatusWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/ActivityStatusWsRequest.java new file mode 100644 index 00000000000..66407145b75 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/ActivityStatusWsRequest.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonarqube.ws.client.ce; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public class ActivityStatusWsRequest { + private final String componentId; + private final String componentKey; + + private ActivityStatusWsRequest(Builder builder) { + this.componentId = builder.componentId; + this.componentKey = builder.componentKey; + } + + @CheckForNull + public String getComponentId() { + return componentId; + } + + @CheckForNull + public String getComponentKey() { + return componentKey; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private String componentId; + private String componentKey; + + private Builder() { + // enforce newBuilder() use for instantiation + } + + public Builder setComponentId(@Nullable String componentId) { + this.componentId = componentId; + return this; + } + + public Builder setComponentKey(@Nullable String componentKey) { + this.componentKey = componentKey; + return this; + } + + public ActivityStatusWsRequest build() { + return new ActivityStatusWsRequest(this); + } + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java index 1871cd800d9..460ddb53faa 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java @@ -28,6 +28,7 @@ import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsConnector; import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_ID; +import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_COMPONENT_KEY; import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_MAX_EXECUTED_AT; import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_MIN_SUBMITTED_AT; import static org.sonarqube.ws.client.ce.CeWsParameters.PARAM_ONLY_CURRENTS; @@ -71,4 +72,13 @@ public class CeService extends BaseService { public WsCe.TaskResponse task(String id) { return call(new GetRequest(path("task")).setParam("id", id), WsCe.TaskResponse.parser()); } + + public WsCe.ActivityStatusWsResponse activityStatus(ActivityStatusWsRequest request) { + return call( + new GetRequest(path("activity_status")) + .setParam(PARAM_COMPONENT_ID, request.getComponentId()) + .setParam(PARAM_COMPONENT_KEY, request.getComponentKey()), + WsCe.ActivityStatusWsResponse.parser()); + } + } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeWsParameters.java index 3b4fc97e4f1..e2254619a2a 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeWsParameters.java @@ -22,6 +22,7 @@ package org.sonarqube.ws.client.ce; public class CeWsParameters { public static final String PARAM_COMPONENT_ID = "componentId"; + public static final String PARAM_COMPONENT_KEY = "componentKey"; public static final String PARAM_COMPONENT_QUERY = "componentQuery"; public static final String PARAM_TYPE = "type"; public static final String PARAM_STATUS = "status"; diff --git a/sonar-ws/src/main/protobuf/ws-ce.proto b/sonar-ws/src/main/protobuf/ws-ce.proto index 126122a0561..4440851e45a 100644 --- a/sonar-ws/src/main/protobuf/ws-ce.proto +++ b/sonar-ws/src/main/protobuf/ws-ce.proto @@ -44,6 +44,12 @@ message ActivityResponse { repeated Task tasks = 2; } +// GET api/ce/activity_status +message ActivityStatusWsResponse { + optional int32 pending = 1; + optional int32 failing = 2; +} + // GET api/ce/project message ProjectResponse { repeated Task queue = 1; -- 2.39.5