You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProjectStatusAction.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.qualitygate.ws;
  21. import java.util.Arrays;
  22. import java.util.Optional;
  23. import java.util.stream.Collectors;
  24. import javax.annotation.Nullable;
  25. import javax.annotation.concurrent.Immutable;
  26. import org.sonar.api.measures.CoreMetrics;
  27. import org.sonar.api.server.ws.Change;
  28. import org.sonar.api.server.ws.Request;
  29. import org.sonar.api.server.ws.Response;
  30. import org.sonar.api.server.ws.WebService;
  31. import org.sonar.api.web.UserRole;
  32. import org.sonar.core.util.Uuids;
  33. import org.sonar.db.DbClient;
  34. import org.sonar.db.DbSession;
  35. import org.sonar.db.component.BranchDto;
  36. import org.sonar.db.component.SnapshotDto;
  37. import org.sonar.db.measure.LiveMeasureDto;
  38. import org.sonar.db.measure.MeasureDto;
  39. import org.sonar.db.permission.GlobalPermission;
  40. import org.sonar.db.project.ProjectDto;
  41. import org.sonar.server.component.ComponentFinder;
  42. import org.sonar.server.exceptions.BadRequestException;
  43. import org.sonar.server.qualitygate.QualityGateCaycChecker;
  44. import org.sonar.server.qualitygate.QualityGateCaycStatus;
  45. import org.sonar.server.user.UserSession;
  46. import org.sonar.server.ws.KeyExamples;
  47. import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
  48. import static com.google.common.base.Strings.isNullOrEmpty;
  49. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  50. import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
  51. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
  52. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
  53. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
  54. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
  55. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
  56. import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
  57. import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
  58. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  59. public class ProjectStatusAction implements QualityGatesWsAction {
  60. private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
  61. .map(Enum::toString)
  62. .collect(Collectors.joining(", "));
  63. private static final String MSG_ONE_PROJECT_PARAMETER_ONLY = String.format("Either '%s', '%s' or '%s' must be provided", PARAM_ANALYSIS_ID, PARAM_PROJECT_ID, PARAM_PROJECT_KEY);
  64. private static final String MSG_ONE_BRANCH_PARAMETER_ONLY = String.format("Either '%s' or '%s' can be provided, not both", PARAM_BRANCH, PARAM_PULL_REQUEST);
  65. private final DbClient dbClient;
  66. private final ComponentFinder componentFinder;
  67. private final UserSession userSession;
  68. private final QualityGateCaycChecker qualityGateCaycChecker;
  69. public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
  70. this.dbClient = dbClient;
  71. this.componentFinder = componentFinder;
  72. this.userSession = userSession;
  73. this.qualityGateCaycChecker = qualityGateCaycChecker;
  74. }
  75. @Override
  76. public void define(WebService.NewController controller) {
  77. WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
  78. .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
  79. "%s <br />" +
  80. "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
  81. "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
  82. "Requires one of the following permissions:" +
  83. "<ul>" +
  84. "<li>'Administer System'</li>" +
  85. "<li>'Administer' rights on the specified project</li>" +
  86. "<li>'Browse' on the specified project</li>" +
  87. "<li>'Execute Analysis' on the specified project</li>" +
  88. "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
  89. .setResponseExample(getClass().getResource("project_status-example.json"))
  90. .setSince("5.3")
  91. .setHandler(this)
  92. .setChangelog(
  93. new Change("9.9", "'caycStatus' field is added to the response"),
  94. new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
  95. new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
  96. new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
  97. new Change("7.6", "The field 'warning' in the response is deprecated"),
  98. new Change("6.4", "The field 'ignoredConditions' is added to the response"));
  99. action.createParam(PARAM_ANALYSIS_ID)
  100. .setDescription("Analysis id")
  101. .setExampleValue(Uuids.UUID_EXAMPLE_04);
  102. action.createParam(PARAM_PROJECT_ID)
  103. .setSince("5.4")
  104. .setDescription("Project UUID. Doesn't work with branches or pull requests")
  105. .setExampleValue(Uuids.UUID_EXAMPLE_01);
  106. action.createParam(PARAM_PROJECT_KEY)
  107. .setSince("5.4")
  108. .setDescription("Project key")
  109. .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
  110. action.createParam(PARAM_BRANCH)
  111. .setSince("7.7")
  112. .setDescription("Branch key")
  113. .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
  114. action.createParam(PARAM_PULL_REQUEST)
  115. .setSince("7.7")
  116. .setDescription("Pull request id")
  117. .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
  118. }
  119. @Override
  120. public void handle(Request request, Response response) throws Exception {
  121. String analysisId = request.param(PARAM_ANALYSIS_ID);
  122. String projectId = request.param(PARAM_PROJECT_ID);
  123. String projectKey = request.param(PARAM_PROJECT_KEY);
  124. String branchKey = request.param(PARAM_BRANCH);
  125. String pullRequestId = request.param(PARAM_PULL_REQUEST);
  126. checkRequest(
  127. !isNullOrEmpty(analysisId)
  128. ^ !isNullOrEmpty(projectId)
  129. ^ !isNullOrEmpty(projectKey),
  130. MSG_ONE_PROJECT_PARAMETER_ONLY);
  131. checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
  132. try (DbSession dbSession = dbClient.openSession(false)) {
  133. ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
  134. writeProtobuf(projectStatusResponse, request, response);
  135. }
  136. }
  137. private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
  138. @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
  139. ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
  140. checkPermission(projectAndSnapshot.project);
  141. Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
  142. QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
  143. return ProjectStatusResponse.newBuilder()
  144. .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), caycStatus).format())
  145. .build();
  146. }
  147. private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
  148. @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
  149. if (!isNullOrEmpty(analysisId)) {
  150. return getSnapshotThenProject(dbSession, analysisId);
  151. }
  152. if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
  153. return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
  154. }
  155. throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
  156. }
  157. private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
  158. @Nullable String branchKey, @Nullable String pullRequestId) {
  159. ProjectDto projectDto;
  160. BranchDto branchDto;
  161. if (projectUuid != null) {
  162. projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
  163. branchDto = componentFinder.getMainBranch(dbSession, projectDto);
  164. } else {
  165. projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
  166. branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
  167. }
  168. Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
  169. return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
  170. }
  171. private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
  172. SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
  173. BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getComponentUuid())
  174. .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
  175. ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
  176. .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
  177. return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
  178. }
  179. private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
  180. Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
  181. return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
  182. }
  183. private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
  184. if (onAnalysis) {
  185. if (!projectAndSnapshot.snapshotDto.isPresent()) {
  186. return Optional.empty();
  187. }
  188. // get the gate status as it was computed during the specified analysis
  189. String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
  190. return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
  191. .map(MeasureDto::getData);
  192. }
  193. // do not restrict to a specified analysis, use the live measure
  194. Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
  195. return measure.map(LiveMeasureDto::getDataAsString);
  196. }
  197. private void checkPermission(ProjectDto project) {
  198. if (!userSession.hasProjectPermission(UserRole.ADMIN, project) &&
  199. !userSession.hasProjectPermission(UserRole.USER, project) &&
  200. !userSession.hasProjectPermission(UserRole.SCAN, project) &&
  201. !userSession.hasPermission(GlobalPermission.SCAN)) {
  202. throw insufficientPrivilegesException();
  203. }
  204. }
  205. @Immutable
  206. private static class ProjectAndSnapshot {
  207. private final BranchDto branch;
  208. private final Optional<SnapshotDto> snapshotDto;
  209. private final ProjectDto project;
  210. private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
  211. this.project = project;
  212. this.branch = branch;
  213. this.snapshotDto = Optional.ofNullable(snapshotDto);
  214. }
  215. }
  216. }