]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19790 Return managed status for project in /api/project/search
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>
Fri, 14 Jul 2023 08:30:31 +0000 (10:30 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 18 Jul 2023 20:03:22 +0000 (20:03 +0000)
server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java
server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java
server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/SearchAction.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/project/ws/search-example.json
sonar-ws/src/main/protobuf/ws-projects.proto

index 49c0d616c6c65789f26fc0773be63f85ee5f42f1..ff0b48937cf867558ec2837d039131ca647fbcef 100644 (file)
@@ -107,6 +107,23 @@ public class ProjectAlmSettingDaoIT {
       .containsExactlyInAnyOrder(githubProject1, githubProject2);
   }
 
+  @Test
+  public void selectByProjectUuidsAndAlm_whenGivenGithubAndProjectUuids_shouldOnlyReturnThose() {
+    AlmSettingDto githubSetting = db.almSettings().insertGitHubAlmSetting();
+    ProjectAlmSettingDto githubProject = createAlmProject(githubSetting);
+    createAlmProject(githubSetting);
+
+    AlmSettingDto gitlabSetting = db.almSettings().insertGitlabAlmSetting();
+    ProjectAlmSettingDto gitlabProject = createAlmProject(gitlabSetting);
+
+    List<ProjectAlmSettingDto> projectAlmSettingDtos =
+      underTest.selectByProjectUuidsAndAlm(dbSession, Set.of(githubProject.getProjectUuid(), gitlabProject.getProjectUuid()), ALM.GITHUB);
+
+    assertThat(projectAlmSettingDtos)
+      .usingRecursiveFieldByFieldElementComparator()
+      .containsExactlyInAnyOrder(githubProject);
+  }
+
   private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto) {
     ProjectDto project = db.components().insertPrivateProject().getProjectDto();
     when(uuidFactory.create()).thenReturn(project.getUuid() + "_forSetting");
index 47f9a3d8f53c947386c5dedb5a45b3bcb47fb9a9..8ca55a8037d82e711818506a6df33beeb680ac30 100644 (file)
@@ -107,6 +107,10 @@ public class ProjectAlmSettingDao implements Dao {
     return getMapper(dbSession).selectByAlm(alm.getId().toLowerCase(Locale.ROOT));
   }
 
+  public List<ProjectAlmSettingDto> selectByProjectUuidsAndAlm(DbSession dbSession, Set<String> projectUuids, ALM alm) {
+    return getMapper(dbSession).selectByProjectUuidsAndAlm(projectUuids, alm.getId().toLowerCase(Locale.ROOT));
+  }
+
   public List<ProjectAlmKeyAndProject> selectAlmTypeAndUrlByProject(DbSession dbSession) {
     return getMapper(dbSession).selectAlmTypeAndUrlByProject();
   }
index b150be296744a4b7951d6c6d02109fa35972af13..c7189d3369e818541fe723744d2acb590f4f2393 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.db.alm.setting;
 
 import java.util.List;
+import java.util.Set;
 import javax.annotation.CheckForNull;
 import org.apache.ibatis.annotations.Param;
 
@@ -43,5 +44,7 @@ public interface ProjectAlmSettingMapper {
 
   List<ProjectAlmSettingDto> selectByAlm(@Param("alm") String alm);
 
+  List<ProjectAlmSettingDto> selectByProjectUuidsAndAlm(@Param("projectUuids") Set<String> projectUuids, @Param("alm") String alm);
+
   List<ProjectAlmKeyAndProject> selectAlmTypeAndUrlByProject();
 }
index 5eca8559d72b66c0cabf44ed19eb0e0e138ee9ed..8977299928408fac3147e51c2346abaf914ed5d6 100644 (file)
@@ -58,7 +58,7 @@
       </foreach>
   </select>
 
-  <select id="selectByAlm" parameterType="string" resultType="org.sonar.db.alm.setting.ProjectAlmSettingDto">
+  <sql id="selectByAlmSql">
     select <include refid="sqlColumns"/>
     from
       project_alm_settings p
       alm_settings alm_settings on p.alm_setting_uuid = alm_settings.uuid
     where
       alm_settings.alm_id=#{alm, jdbcType=VARCHAR}
+  </sql>
+
+  <select id="selectByAlm" parameterType="string" resultType="org.sonar.db.alm.setting.ProjectAlmSettingDto">
+    <include refid="selectByAlmSql"/>
+  </select>
+
+  <select id="selectByProjectUuidsAndAlm" parameterType="map" resultType="org.sonar.db.alm.setting.ProjectAlmSettingDto">
+    <include refid="selectByAlmSql"/>
+    and p.project_uuid in
+      <foreach collection="projectUuids" open="(" close=")" item="projectUuid" separator=",">
+        #{projectUuid, jdbcType=VARCHAR}
+      </foreach>
   </select>
 
   <insert id="insert" parameterType="Map" useGeneratedKeys="false">
index 7234e84a8936dd451079461c5928ff9cb59dd2bb..2a76ad5779ceab24be8fa2c0742b61f81cf36944 100644 (file)
@@ -110,6 +110,13 @@ public class DelegatingManagedServices implements ManagedInstanceService, Manage
     return resourcesUuid.stream().collect(toMap(identity(), any -> false));
   }
 
+  @Override
+  public Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids) {
+    return findManagedProjectService()
+      .map(managedProjectService -> managedProjectService.getProjectUuidToManaged(dbSession, projectUuids))
+      .orElse(returnNonManagedForAll(projectUuids));
+  }
+
   @Override
   public boolean isProjectManaged(DbSession dbSession, String projectUuid) {
     return findManagedProjectService()
index 101e245402071909c421d32c207d953b5327aaa4..4679195554e66b2745cca4aac59349845308cf0d 100644 (file)
  */
 package org.sonar.server.management;
 
+import java.util.Map;
+import java.util.Set;
 import org.sonar.db.DbSession;
 
 public interface ManagedProjectService {
 
+  Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids);
+
   boolean isProjectManaged(DbSession dbSession, String projectUuid);
 
 }
index e7a7bbccfd8f1f3628d6294e59cd5f6e6466d5bb..44ee3c5c6742cb0602db12846ad98993dbfa8184 100644 (file)
@@ -31,8 +31,10 @@ import static java.util.Collections.emptySet;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
 
 @RunWith(MockitoJUnitRunner.class)
 public class DelegatingManagedServicesTest {
@@ -216,6 +218,36 @@ public class DelegatingManagedServicesTest {
     return anotherManagedInstanceService;
   }
 
+  @Test
+  public void getProjectUuidToManaged_whenNoDelegates_setAllProjectsAsNonManaged() {
+    Set<String> projectUuids = Set.of("a", "b");
+    DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES;
+
+    Map<String, Boolean> projectUuidToManaged = managedInstanceService.getProjectUuidToManaged(dbSession, projectUuids);
+
+    assertThat(projectUuidToManaged).containsExactlyInAnyOrderEntriesOf(Map.of("a", false, "b", false));
+  }
+
+  @Test
+  public void getProjectUuidToManaged_delegatesToRightService_andPropagateAnswer() {
+    Set<String> projectUuids = Set.of("a", "b");
+    Map<String, Boolean> serviceResponse = Map.of("a", false, "b", true);
+
+    ManagedInstanceService anotherManagedProjectService = getManagedProjectService(projectUuids, serviceResponse);
+    DelegatingManagedServices managedInstanceService = new DelegatingManagedServices(Set.of(new NeverManagedInstanceService(), anotherManagedProjectService));
+
+    Map<String, Boolean> projectUuidToManaged = managedInstanceService.getProjectUuidToManaged(dbSession, projectUuids);
+
+    assertThat(projectUuidToManaged).containsExactlyInAnyOrderEntriesOf(serviceResponse);
+  }
+
+  private ManagedInstanceService getManagedProjectService(Set<String> projectUuids, Map<String, Boolean> uuidsToManaged) {
+    ManagedInstanceService anotherManagedProjectService = mock(ManagedInstanceService.class, withSettings().extraInterfaces(ManagedProjectService.class));
+    when(anotherManagedProjectService.isInstanceExternallyManaged()).thenReturn(true);
+    doReturn(uuidsToManaged).when((ManagedProjectService) anotherManagedProjectService).getProjectUuidToManaged(dbSession, projectUuids);
+    return anotherManagedProjectService;
+  }
+
   @Test
   public void isProjectManaged_whenManagedInstanceServices_shouldDelegatesToRightService() {
     DelegatingManagedServices managedInstanceService = new DelegatingManagedServices(Set.of(new NeverManagedInstanceService(), new AlwaysManagedInstanceService()));
@@ -272,6 +304,11 @@ public class DelegatingManagedServicesTest {
       return false;
     }
 
+    @Override
+    public Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids) {
+      return null;
+    }
+
     @Override
     public boolean isProjectManaged(DbSession dbSession, String projectUuid) {
       return false;
@@ -320,6 +357,11 @@ public class DelegatingManagedServicesTest {
       return true;
     }
 
+    @Override
+    public Map<String, Boolean> getProjectUuidToManaged(DbSession dbSession, Set<String> projectUuids) {
+      return null;
+    }
+
     @Override
     public boolean isProjectManaged(DbSession dbSession, String projectUuid) {
       return true;
index a21e4e76885b4fc48918594a342eac74d6a3a666..188c322abe5e07bf23bd9978f261e764fb5f5aa5 100644 (file)
@@ -26,6 +26,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
@@ -37,6 +39,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ProjectData;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.management.ManagedProjectService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
@@ -51,6 +54,10 @@ import static java.util.Optional.ofNullable;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
 import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
@@ -79,7 +86,9 @@ public class SearchActionIT {
   @Rule
   public final DbTester db = DbTester.create();
 
-  private final WsActionTester ws = new WsActionTester(new SearchAction(db.getDbClient(), userSession));
+  private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class);
+
+  private final WsActionTester ws = new WsActionTester(new SearchAction(db.getDbClient(), userSession, managedProjectService));
 
   @Test
   public void search_by_key_query_with_partial_match_case_insensitive() {
@@ -203,6 +212,26 @@ public class SearchActionIT {
     assertThat(result.getComponentsList()).isEmpty();
   }
 
+  @Test
+  public void search_return_manage_status() {
+    userSession.addPermission(ADMINISTER);
+    ProjectData managedProject = db.components().insertPrivateProject();
+    ProjectData notManagedProject = db.components().insertPrivateProject();
+
+    when(managedProjectService.getProjectUuidToManaged(any(), eq(Set.of(managedProject.projectUuid(), notManagedProject.projectUuid()))))
+      .thenReturn(Map.of(
+        managedProject.projectUuid(), true,
+        notManagedProject.projectUuid(), false
+      ));
+
+    SearchWsResponse result = call(SearchRequest.builder().build());
+    assertThat(result.getComponentsList())
+      .extracting(Component::getKey, Component::getManaged)
+      .containsExactlyInAnyOrder(
+        tuple(managedProject.projectKey(), true),
+        tuple(notManagedProject.projectKey(), false));
+  }
+
   private static String toStringAtUTC(Date d) {
     OffsetDateTime offsetTime = d.toInstant().atOffset(ZoneOffset.UTC);
     return DateUtils.formatDateTime(offsetTime);
@@ -346,7 +375,7 @@ public class SearchActionIT {
   public void json_example() {
     userSession.addPermission(ADMINISTER);
     ProjectData publicProject = db.components().insertPublicProject("project-uuid-1", p -> p.setName("Project Name 1").setKey("project-key-1").setPrivate(false));
-    ProjectData privateProject = db.components().insertPrivateProject("project-uuid-2", p -> p.setName("Project Name 1").setKey("project-key-2"));
+    ProjectData privateProject = db.components().insertPrivateProject("project-uuid-2", p -> p.setName("Project Name 2").setKey("project-key-2"));
     db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(publicProject.getMainBranchDto())
       .setCreatedAt(parseDateTime("2017-03-01T11:39:03+0300").getTime())
       .setRevision("cfb82f55c6ef32e61828c4cb3db2da12795fd767"));
index c6a746b70c223c9d01dc6eceb5f0e1f3405502f1..2114ee48fdf294c6a7db23b906ab919bbb0c2281 100644 (file)
@@ -40,12 +40,14 @@ import org.sonar.db.component.ComponentQuery;
 import org.sonar.db.component.ProjectLastAnalysisDateDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.permission.GlobalPermission;
+import org.sonar.server.management.ManagedProjectService;
 import org.sonar.server.project.Visibility;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Projects.SearchWsResponse;
 
 import static java.util.Optional.ofNullable;
 import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
 import static org.sonar.api.resources.Qualifiers.APP;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.api.resources.Qualifiers.VIEW;
@@ -70,10 +72,12 @@ public class SearchAction implements ProjectsWsAction {
 
   private final DbClient dbClient;
   private final UserSession userSession;
+  private final ManagedProjectService managedProjectService;
 
-  public SearchAction(DbClient dbClient, UserSession userSession) {
+  public SearchAction(DbClient dbClient, UserSession userSession, ManagedProjectService managedProjectService) {
     this.dbClient = dbClient;
     this.userSession = userSession;
+    this.managedProjectService = managedProjectService;
   }
 
   @Override
@@ -84,6 +88,7 @@ public class SearchAction implements ProjectsWsAction {
         "Requires 'Administer System' permission")
       .addPagingParams(100, MAX_PAGE_SIZE)
       .setResponseExample(getClass().getResource("search-example.json"))
+      .setChangelog(new Change("10.2", "Response includes 'managed' field."))
       .setChangelog(new Change("9.1", "The parameter '" + PARAM_ANALYZED_BEFORE + "' and the field 'lastAnalysisDate' of the returned projects "
         + "take into account the analysis of all branches and pull requests, not only the main branch."))
       .setHandler(this);
@@ -160,17 +165,31 @@ public class SearchAction implements ProjectsWsAction {
       List<ComponentDto> components = dbClient.componentDao().selectByQuery(dbSession, query, paging.offset(), paging.pageSize());
       Set<String> componentUuids = components.stream().map(ComponentDto::uuid).collect(Collectors.toSet());
       List<BranchDto> branchDtos = dbClient.branchDao().selectByUuids(dbSession, componentUuids);
-      Map<String, String> projectUuidByComponentUuid = branchDtos.stream().collect(Collectors.toMap(BranchDto::getUuid,BranchDto::getProjectUuid));
-      Map<String, Long> lastAnalysisDateByComponentUuid = dbClient.snapshotDao().selectLastAnalysisDateByProjectUuids(dbSession, projectUuidByComponentUuid.values()).stream()
+      Map<String, String> componentUuidToProjectUuid = branchDtos.stream().collect(Collectors.toMap(BranchDto::getUuid,BranchDto::getProjectUuid));
+      Map<String, Boolean> projectUuidToManaged = managedProjectService.getProjectUuidToManaged(dbSession, new HashSet<>(componentUuidToProjectUuid.values()));
+      Map<String, Boolean> componentUuidToManaged = toComponentUuidToManaged(componentUuidToProjectUuid, projectUuidToManaged);
+      Map<String, Long> lastAnalysisDateByComponentUuid = dbClient.snapshotDao().selectLastAnalysisDateByProjectUuids(dbSession, componentUuidToProjectUuid.values()).stream()
         .collect(Collectors.toMap(ProjectLastAnalysisDateDto::getProjectUuid, ProjectLastAnalysisDateDto::getDate));
       Map<String, SnapshotDto> snapshotsByComponentUuid = dbClient.snapshotDao()
         .selectLastAnalysesByRootComponentUuids(dbSession, componentUuids).stream()
         .collect(Collectors.toMap(SnapshotDto::getRootComponentUuid, identity()));
 
-      return buildResponse(components, snapshotsByComponentUuid, lastAnalysisDateByComponentUuid, projectUuidByComponentUuid, paging);
+      return buildResponse(components, snapshotsByComponentUuid, lastAnalysisDateByComponentUuid, componentUuidToProjectUuid, componentUuidToManaged, paging);
     }
   }
 
+  private Map<String, Boolean> toComponentUuidToManaged(Map<String, String> componentUuidToProjectUuid, Map<String, Boolean> projectUuidToManaged) {
+    return componentUuidToProjectUuid.keySet().stream()
+      .collect(toMap(identity(), componentUuid -> isComponentManaged(
+        componentUuidToProjectUuid.get(componentUuid),
+        projectUuidToManaged))
+      );
+  }
+
+  private boolean isComponentManaged(String projectUuid, Map<String, Boolean> projectUuidToManaged) {
+    return ofNullable(projectUuidToManaged.get(projectUuid)).orElse(false);
+  }
+
   static ComponentQuery buildDbQuery(SearchRequest request) {
     List<String> qualifiers = request.getQualifiers();
     ComponentQuery.Builder query = ComponentQuery.builder()
@@ -196,7 +215,7 @@ public class SearchAction implements ProjectsWsAction {
   }
 
   private static SearchWsResponse buildResponse(List<ComponentDto> components, Map<String, SnapshotDto> snapshotsByComponentUuid,
-    Map<String, Long> lastAnalysisDateByComponentUuid, Map<String, String> projectUuidByComponentUuid, Paging paging) {
+    Map<String, Long> lastAnalysisDateByComponentUuid, Map<String, String> projectUuidByComponentUuid, Map<String, Boolean> componentUuidToManaged, Paging paging) {
     SearchWsResponse.Builder responseBuilder = newBuilder();
     responseBuilder.getPagingBuilder()
       .setPageIndex(paging.pageIndex())
@@ -205,12 +224,13 @@ public class SearchAction implements ProjectsWsAction {
       .build();
 
     components.stream()
-      .map(dto -> dtoToProject(dto, snapshotsByComponentUuid.get(dto.uuid()), lastAnalysisDateByComponentUuid.get(projectUuidByComponentUuid.get(dto.uuid()))))
+      .map(dto -> dtoToProject(dto, snapshotsByComponentUuid.get(dto.uuid()), lastAnalysisDateByComponentUuid.get(projectUuidByComponentUuid.get(dto.uuid())),
+        PROJECT.equals(dto.qualifier()) ? componentUuidToManaged.get(dto.uuid()) : null))
       .forEach(responseBuilder::addComponents);
     return responseBuilder.build();
   }
 
-  private static Component dtoToProject(ComponentDto dto, @Nullable SnapshotDto snapshot, @Nullable Long lastAnalysisDate) {
+  private static Component dtoToProject(ComponentDto dto, @Nullable SnapshotDto snapshot, @Nullable Long lastAnalysisDate, @Nullable Boolean isManaged) {
     Component.Builder builder = Component.newBuilder()
       .setKey(dto.getKey())
       .setName(dto.name())
@@ -220,6 +240,7 @@ public class SearchAction implements ProjectsWsAction {
       ofNullable(lastAnalysisDate).ifPresent(d -> builder.setLastAnalysisDate(formatDateTime(d)));
       ofNullable(snapshot.getRevision()).ifPresent(builder::setRevision);
     }
+    ofNullable(isManaged).ifPresent(builder::setManaged);
 
     return builder.build();
   }
index e623ea2a52fd1fc29b7bf709999c800261cec7ed..5da1b2fb343bb611e4b8d08806982ec2ef16e922 100644 (file)
       "qualifier": "TRK",
       "visibility": "public",
       "lastAnalysisDate": "2017-03-01T11:39:03+0300",
-      "revision": "cfb82f55c6ef32e61828c4cb3db2da12795fd767"
+      "revision": "cfb82f55c6ef32e61828c4cb3db2da12795fd767",
+      "managed": false
     },
     {
       "key": "project-key-2",
-      "name": "Project Name 1",
+      "name": "Project Name 2",
       "qualifier": "TRK",
       "visibility": "private",
       "lastAnalysisDate": "2017-03-02T15:21:47+0300",
-      "revision": "7be96a94ac0c95a61ee6ee0ef9c6f808d386a355"
+      "revision": "7be96a94ac0c95a61ee6ee0ef9c6f808d386a355",
+      "managed": false
     }
   ]
 }
index 5fc3b07942c48acc842047ba66b03493e99d8b21..0514fd06d8e1ea8d9ce6716d52e1d0a90adc4659 100644 (file)
@@ -71,6 +71,7 @@ message SearchWsResponse {
     optional string visibility = 6;
     optional string lastAnalysisDate = 7;
     optional string revision = 8;
+    optional bool managed = 9;
   }
 }