diff options
author | alain <108417558+alain-kermis-sonarsource@users.noreply.github.com> | 2022-10-27 14:08:20 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-10-27 20:03:02 +0000 |
commit | 83bd18d97d9d8e0b858875b253bc52efff70e810 (patch) | |
tree | 6984dfa8964d739eafa615aa9434cb933a899f3e | |
parent | 0806043b590e1d0f3bf08c4d88627dc9bb6cf913 (diff) | |
download | sonarqube-83bd18d97d9d8e0b858875b253bc52efff70e810.tar.gz sonarqube-83bd18d97d9d8e0b858875b253bc52efff70e810.zip |
SONAR-17497 Add web service to reindex issues
13 files changed, 397 insertions, 25 deletions
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/issue/index/NoAsyncIssueIndexing.java b/server/sonar-ce/src/main/java/org/sonar/ce/issue/index/NoAsyncIssueIndexing.java index 6ade7075c05..762ae11f31b 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/issue/index/NoAsyncIssueIndexing.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/issue/index/NoAsyncIssueIndexing.java @@ -28,4 +28,9 @@ public class NoAsyncIssueIndexing implements AsyncIssueIndexing { public void triggerOnIndexCreation() { throw new IllegalStateException("Async issue indexing should not be triggered in Compute Engine"); } + + @Override + public void triggerForProject(String projectUuid) { + throw new IllegalStateException("Async issue indexing should not be triggered in Compute Engine"); + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java index f655c175cfd..39242c80ac6 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java @@ -148,10 +148,18 @@ public class BranchDao implements Dao { return mapper(dbSession).selectBranchNeedingIssueSync(); } + public List<BranchDto> selectBranchNeedingIssueSyncForProject(DbSession dbSession, String projectUuid) { + return mapper(dbSession).selectBranchNeedingIssueSyncForProject(projectUuid); + } + public long updateAllNeedIssueSync(DbSession dbSession) { return mapper(dbSession).updateAllNeedIssueSync(system2.now()); } + public long updateAllNeedIssueSyncForProject(DbSession dbSession, String projectUuid) { + return mapper(dbSession).updateAllNeedIssueSyncForProject(projectUuid, system2.now()); + } + public long updateNeedIssueSync(DbSession dbSession, String branchUuid, boolean needIssueSync) { long now = system2.now(); return mapper(dbSession).updateNeedIssueSync(branchUuid, needIssueSync, now); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java index 9f19580941c..691ef746cbd 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java @@ -65,8 +65,12 @@ public interface BranchMapper { List<BranchDto> selectBranchNeedingIssueSync(); + List<BranchDto> selectBranchNeedingIssueSyncForProject(@Param("projectUuid") String projectUuid); + long updateAllNeedIssueSync(@Param("now") long now); + long updateAllNeedIssueSyncForProject(@Param("projectUuid") String projectUuid, @Param("now") long now); + long updateNeedIssueSync(@Param("uuid") String uuid, @Param("needIssueSync")boolean needIssueSync,@Param("now") long now); short doAnyOfComponentsNeedIssueSync(@Param("componentKeys") List<String> components); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml index 246dc8bbf43..fcfae3b120a 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml @@ -205,6 +205,14 @@ order by pb.updated_at desc, uuid </select> + <select id="selectBranchNeedingIssueSyncForProject" resultType="org.sonar.db.component.BranchDto"> + select + <include refid="columns"/> + from project_branches pb + where need_issue_sync = ${_true} and project_uuid = #{projectUuid, jdbcType=VARCHAR} + order by pb.updated_at desc, uuid + </select> + <update id="updateAllNeedIssueSync"> update project_branches set @@ -212,6 +220,15 @@ updated_at = #{now, jdbcType=BIGINT} </update> + <update id="updateAllNeedIssueSyncForProject"> + update project_branches + set + need_issue_sync = ${_true}, + updated_at = #{now, jdbcType=BIGINT} + where + project_uuid = #{projectUuid, jdbcType=VARCHAR} + </update> + <update id="updateNeedIssueSync"> update project_branches set diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java index 5de47d10d24..1c6589f3190 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java @@ -237,7 +237,7 @@ public class BranchDaoTest { assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch); assertThat(loadedPullRequestData.getTitle()).isEqualTo(title); assertThat(loadedPullRequestData.getUrl()).isEqualTo(url); - assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue); + assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue); } @Test @@ -303,7 +303,7 @@ public class BranchDaoTest { assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch); assertThat(loadedPullRequestData.getTitle()).isEqualTo(title); assertThat(loadedPullRequestData.getUrl()).isEqualTo(url); - assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue); + assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue); } @Test @@ -356,7 +356,7 @@ public class BranchDaoTest { assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch); assertThat(loadedPullRequestData.getTitle()).isEqualTo(title); assertThat(loadedPullRequestData.getUrl()).isEqualTo(url); - assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue); + assertThat(loadedPullRequestData.getAttributesMap()).containsEntry(tokenAttributeName, tokenAttributeValue); } @Test @@ -680,6 +680,17 @@ public class BranchDaoTest { } @Test + public void selectBranchNeedingIssueSyncForProject() { + ComponentDto project = db.components().insertPrivateProject(); + String uuid = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(true)).uuid(); + db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(false)); + + assertThat(underTest.selectBranchNeedingIssueSyncForProject(dbSession, project.uuid())) + .extracting(BranchDto::getUuid) + .containsExactly(uuid); + } + + @Test public void updateAllNeedIssueSync() { ComponentDto project = db.components().insertPrivateProject(); String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(true)).uuid(); @@ -697,6 +708,23 @@ public class BranchDaoTest { } @Test + public void updateAllNeedIssueSyncForProject() { + ComponentDto project = db.components().insertPrivateProject(); + String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(true)).uuid(); + String uuid2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(false)).uuid(); + + underTest.updateAllNeedIssueSyncForProject(dbSession, project.uuid()); + + Optional<BranchDto> project1 = underTest.selectByUuid(dbSession, uuid1); + assertThat(project1).isPresent(); + assertThat(project1.get().isNeedIssueSync()).isTrue(); + + Optional<BranchDto> project2 = underTest.selectByUuid(dbSession, uuid2); + assertThat(project2).isPresent(); + assertThat(project2.get().isNeedIssueSync()).isTrue(); + } + + @Test public void updateNeedIssueSync() { ComponentDto project = db.components().insertPrivateProject(); String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setNeedIssueSync(false)).uuid(); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexing.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexing.java index 35f5de4f29d..cb0221e44f4 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexing.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexing.java @@ -21,4 +21,5 @@ package org.sonar.server.issue.index; public interface AsyncIssueIndexing { void triggerOnIndexCreation(); + void triggerForProject(String projectUuid); } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexer.java index 308f6a21d97..72246142b91 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexer.java @@ -116,6 +116,10 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer { } } + public void indexProject(String projectUuid) { + asyncIssueIndexing.triggerForProject(projectUuid); + } + @Override public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) { switch (cause) { diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexingImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexingImpl.java index 322d91037ca..bf92288a2a7 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexingImpl.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/issue/index/AsyncIssueIndexingImpl.java @@ -96,6 +96,27 @@ public class AsyncIssueIndexingImpl implements AsyncIssueIndexing { } } + @Override + public void triggerForProject(String projectUuid) { + try (DbSession dbSession = dbClient.openSession(false)) { + + // remove already existing indexation task, if any + removeExistingIndexationTasksForProject(dbSession, projectUuid); + + dbClient.branchDao().updateAllNeedIssueSyncForProject(dbSession, projectUuid); + List<BranchDto> branchInNeedOfIssueSync = dbClient.branchDao().selectBranchNeedingIssueSyncForProject(dbSession, projectUuid); + LOG.info("{} branch(es) found in need of issue sync for project.", branchInNeedOfIssueSync.size()); + + List<CeTaskSubmit> tasks = new ArrayList<>(); + for (BranchDto branch : branchInNeedOfIssueSync) { + tasks.add(buildTaskSubmit(branch)); + } + + ceQueue.massSubmit(tasks); + dbSession.commit(); + } + } + private void sortProjectUuids(DbSession dbSession, List<String> projectUuids) { Map<String, SnapshotDto> snapshotByProjectUuid = dbClient.snapshotDao() .selectLastAnalysesByRootComponentUuids(dbSession, projectUuids).stream() @@ -122,10 +143,19 @@ public class AsyncIssueIndexingImpl implements AsyncIssueIndexing { } private void removeExistingIndexationTasks(DbSession dbSession) { - List<String> uuids = dbClient.ceQueueDao().selectAllInAscOrder(dbSession).stream() + removeIndexationTasks(dbSession, dbClient.ceQueueDao().selectAllInAscOrder(dbSession)); + } + + private void removeExistingIndexationTasksForProject(DbSession dbSession, String projectUuid) { + removeIndexationTasks(dbSession, dbClient.ceQueueDao().selectByMainComponentUuid(dbSession, projectUuid)); + } + + private void removeIndexationTasks(DbSession dbSession, List<CeQueueDto> ceQueueDtos) { + List<String> uuids = ceQueueDtos.stream() .filter(p -> p.getTaskType().equals(BRANCH_ISSUE_SYNC)) .map(CeQueueDto::getUuid) .collect(Collectors.toList()); + LOG.info(String.format("%s pending indexation task found to be deleted...", uuids.size())); for (String uuid : uuids) { dbClient.ceQueueDao().deleteByUuid(dbSession, uuid); diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexingImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexingImplTest.java index adc62e4b41b..a97fea02e85 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexingImplTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/issue/index/AsyncIssueIndexingImplTest.java @@ -20,7 +20,6 @@ package org.sonar.server.issue.index; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -46,6 +45,7 @@ import org.sonar.db.ce.CeQueueDto; import org.sonar.db.ce.CeTaskCharacteristicDto; import org.sonar.db.component.BranchDto; import org.sonar.db.component.SnapshotDto; +import org.sonar.db.project.ProjectDto; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -69,9 +69,9 @@ public class AsyncIssueIndexingImplTest { @Rule public LogTester logTester = new LogTester(); - private DbClient dbClient = dbTester.getDbClient(); - private CeQueue ceQueue = mock(CeQueue.class); - private UuidFactory uuidFactory = new SequenceUuidFactory(); + private final DbClient dbClient = dbTester.getDbClient(); + private final CeQueue ceQueue = mock(CeQueue.class); + private final UuidFactory uuidFactory = new SequenceUuidFactory(); private final AsyncIssueIndexingImpl underTest = new AsyncIssueIndexingImpl(ceQueue, dbClient); @@ -102,6 +102,27 @@ public class AsyncIssueIndexingImplTest { } @Test + public void triggerForProject() { + ProjectDto projectDto = dbTester.components().insertPrivateProjectDto(); + BranchDto dto = new BranchDto() + .setBranchType(BRANCH) + .setKey("branchName") + .setUuid("branch_uuid") + .setProjectUuid(projectDto.getUuid()); + dbTester.components().insertProjectBranch(projectDto, dto); + + underTest.triggerForProject(projectDto.getUuid()); + + Optional<BranchDto> branch = dbClient.branchDao().selectByUuid(dbTester.getSession(), "branch_uuid"); + assertThat(branch).isPresent(); + assertThat(branch.get().isNeedIssueSync()).isTrue(); + verify(ceQueue, times(2)).prepareSubmit(); + verify(ceQueue, times(1)).massSubmit(anyCollection()); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("2 branch(es) found in need of issue sync for project."); + } + + @Test public void triggerOnIndexCreation_no_branch() { underTest.triggerOnIndexCreation(); @@ -109,36 +130,27 @@ public class AsyncIssueIndexingImplTest { } @Test + public void triggerForProject_no_branch() { + underTest.triggerForProject("some-random-uuid"); + assertThat(logTester.logs(LoggerLevel.INFO)).contains("0 branch(es) found in need of issue sync for project."); + } + + @Test public void remove_existing_indexation_task() { - CeQueueDto reportTask = new CeQueueDto(); - reportTask.setUuid("uuid_1"); - reportTask.setTaskType(REPORT); - dbClient.ceQueueDao().insert(dbTester.getSession(), reportTask); + String reportTaskUuid = persistReportTasks(); - CeActivityDto reportActivity = new CeActivityDto(reportTask); - reportActivity.setStatus(Status.SUCCESS); - dbClient.ceActivityDao().insert(dbTester.getSession(), reportActivity); CeQueueDto task = new CeQueueDto(); task.setUuid("uuid_2"); task.setTaskType(BRANCH_ISSUE_SYNC); dbClient.ceQueueDao().insert(dbTester.getSession(), task); - CeActivityDto activityDto = new CeActivityDto(task); activityDto.setStatus(Status.SUCCESS); dbClient.ceActivityDao().insert(dbTester.getSession(), activityDto); - dbTester.commit(); underTest.triggerOnIndexCreation(); - assertThat(dbClient.ceQueueDao().selectAllInAscOrder(dbTester.getSession())).extracting("uuid") - .containsExactly(reportTask.getUuid()); - assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), BRANCH_ISSUE_SYNC)).isEmpty(); - - assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), REPORT)).hasSize(1); - - assertThat(dbClient.ceTaskCharacteristicsDao().selectByTaskUuids(dbTester.getSession(), new HashSet<>(Arrays.asList("uuid_2")))).isEmpty(); - + assertCeTasks(reportTaskUuid); assertThat(logTester.logs(LoggerLevel.INFO)) .contains( "1 pending indexation task found to be deleted...", @@ -149,6 +161,35 @@ public class AsyncIssueIndexingImplTest { } @Test + public void remove_existing_indexation_for_project_task() { + String reportTaskUuid = persistReportTasks(); + + ProjectDto projectDto = dbTester.components().insertPrivateProjectDto(); + String branchUuid = "branch_uuid"; + dbTester.components().insertProjectBranch(projectDto, b -> b.setBranchType(BRANCH).setUuid(branchUuid)); + CeQueueDto mainBranchTask = new CeQueueDto().setUuid("uuid_2").setTaskType(BRANCH_ISSUE_SYNC) + .setMainComponentUuid(projectDto.getUuid()).setComponentUuid(projectDto.getUuid()); + CeQueueDto branchTask = new CeQueueDto().setUuid("uuid_3").setTaskType(BRANCH_ISSUE_SYNC) + .setMainComponentUuid(projectDto.getUuid()).setComponentUuid(branchUuid); + dbClient.ceQueueDao().insert(dbTester.getSession(), mainBranchTask); + dbClient.ceQueueDao().insert(dbTester.getSession(), branchTask); + dbTester.commit(); + + underTest.triggerForProject(projectDto.getUuid()); + + assertCeTasks(reportTaskUuid); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains( + "2 pending indexation task found to be deleted...", + "2 completed indexation task found to be deleted...", + "Indexation task deletion complete.", + "Deleting tasks characteristics...", + "Tasks characteristics deletion complete.", + "Tasks characteristics deletion complete.", + "2 branch(es) found in need of issue sync for project."); + } + + @Test public void order_by_last_analysis_date() { BranchDto dto = new BranchDto() .setBranchType(BRANCH) @@ -274,4 +315,24 @@ public class AsyncIssueIndexingImplTest { return snapshot; } + private String persistReportTasks() { + CeQueueDto reportTask = new CeQueueDto(); + reportTask.setUuid("uuid_1"); + reportTask.setTaskType(REPORT); + dbClient.ceQueueDao().insert(dbTester.getSession(), reportTask); + + CeActivityDto reportActivity = new CeActivityDto(reportTask); + reportActivity.setStatus(Status.SUCCESS); + dbClient.ceActivityDao().insert(dbTester.getSession(), reportActivity); + return reportTask.getUuid(); + } + + private void assertCeTasks(String reportTaskUuid) { + assertThat(dbClient.ceQueueDao().selectAllInAscOrder(dbTester.getSession())).extracting("uuid") + .containsExactly(reportTaskUuid); + assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), BRANCH_ISSUE_SYNC)).isEmpty(); + assertThat(dbClient.ceActivityDao().selectByTaskType(dbTester.getSession(), REPORT)).hasSize(1); + assertThat(dbClient.ceTaskCharacteristicsDao().selectByTaskUuids(dbTester.getSession(), new HashSet<>(List.of("uuid_2")))).isEmpty(); + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index 37b3a53adbb..6bf37943b81 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -67,6 +67,7 @@ public class IssueWsModule extends Module { SetTagsAction.class, SetTypeAction.class, ComponentTagsAction.class, + ReindexAction.class, AuthorsAction.class, ChangelogAction.class, BulkChangeAction.class, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java new file mode 100644 index 00000000000..2d2a2618528 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ReindexAction.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info 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.issue.ws; + +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.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.index.IssueIndexer; +import org.sonar.server.user.UserSession; + +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT; + +/** + * Implementation of the {@code reindex} action for the Issues WebService. + */ +public class ReindexAction implements IssuesWsAction { + + private static final String ACTION = "reindex"; + private final DbClient dbClient; + private final IssueIndexer issueIndexer; + private final UserSession userSession; + + public ReindexAction(DbClient dbClient, IssueIndexer indexer, UserSession userSession) { + this.dbClient = dbClient; + this.issueIndexer = indexer; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context + .createAction(ACTION) + .setPost(true) + .setDescription("Reindex issues for a project.<br> " + + "Requires one of the following permissions: " + + "<ul>" + + "<li>'Administer System'</li>" + + "<li>'Administer' rights on the specified project</li>" + + "</ul>") + .setSince("9.8") + .setHandler(this); + + action + .createParam(PARAM_PROJECT) + .setDescription("Project key") + .setRequired(true) + .setExampleValue(KEY_PROJECT_EXAMPLE_001); + } + + @Override + public void handle(Request request, Response response) throws Exception { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + + ProjectDto projectDto; + try (DbSession dbSession = dbClient.openSession(false)) { + projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey).orElseThrow(() -> new NotFoundException("project not found")); + userSession.checkProjectPermission(UserRole.ADMIN, projectDto); + } + + issueIndexer.indexProject(projectDto.getUuid()); + response.noContent(); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java new file mode 100644 index 00000000000..fb18d387348 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ReindexActionTest.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info 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.issue.ws; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbTester; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.index.AsyncIssueIndexing; +import org.sonar.server.issue.index.IssueIndexer; +import org.sonar.server.issue.index.IssueIteratorFactory; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ReindexActionTest { + + @Rule + public EsTester es = EsTester.create(); + @Rule + public DbTester db = DbTester.create(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private final AsyncIssueIndexing mock = mock(AsyncIssueIndexing.class); + private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), mock); + private final ReindexAction underTest = new ReindexAction(db.getDbClient(), issueIndexer, userSession); + private final WsActionTester tester = new WsActionTester(underTest); + + @Test + public void test_definition() { + WebService.Action action = tester.getDef(); + + assertThat(action.key()).isEqualTo("reindex"); + assertThat(action.isPost()).isTrue(); + assertThat(action.isInternal()).isFalse(); + assertThat(action.params()).extracting(WebService.Param::key).containsExactly("project"); + } + + @Test + public void reindex_project() { + ProjectDto project = db.components().insertPrivateProjectDto(); + userSession.logIn().setSystemAdministrator(); + userSession.addProjectPermission(UserRole.ADMIN, project); + + TestResponse response = tester.newRequest() + .setParam("project", project.getKey()) + .execute(); + + assertThat(response.getStatus()).isEqualTo(204); + verify(mock, times(1)).triggerForProject(project.getUuid()); + } + + @Test + public void fail_if_project_does_not_exist() { + userSession.logIn().setSystemAdministrator(); + + TestRequest testRequest = tester.newRequest().setParam("project", "some-key"); + assertThatThrownBy(testRequest::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("project not found"); + } + + @Test + public void fail_if_parameter_not_present() { + userSession.anonymous(); + TestRequest testRequest = tester.newRequest(); + assertThatThrownBy(testRequest::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The 'project' parameter is missing"); + } + + @Test + public void fail_if_not_authorized() { + ProjectDto project = db.components().insertPrivateProjectDto(); + userSession.addProjectPermission(UserRole.USER, project); + + TestRequest testRequest = tester.newRequest().setParam("project", project.getKey()); + assertThatThrownBy(testRequest::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); + } + +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java index bd8295956c5..a607c6c8c18 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java @@ -195,6 +195,19 @@ public class IssuesService extends BaseService { /** * * This is part of the internal API. + * This is a POST request. + * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/issues/reindex">Further information about this action online (including a response example)</a> + * @since 9.8 + */ + public void reindex() { + call( + new PostRequest(path("reindex")) + .setMediaType(MediaTypes.JSON)).content(); + } + + /** + * + * This is part of the internal API. * This is a GET request. * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/issues/search">Further information about this action online (including a response example)</a> * @since 3.6 |