3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.server.qualitygate.ws;
22 import java.util.Arrays;
23 import java.util.Optional;
24 import java.util.stream.Collectors;
25 import javax.annotation.Nullable;
26 import javax.annotation.concurrent.Immutable;
27 import org.sonar.api.measures.CoreMetrics;
28 import org.sonar.api.server.ws.Change;
29 import org.sonar.api.server.ws.Request;
30 import org.sonar.api.server.ws.Response;
31 import org.sonar.api.server.ws.WebService;
32 import org.sonar.api.web.UserRole;
33 import org.sonar.core.util.Uuids;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.component.BranchDto;
37 import org.sonar.db.component.SnapshotDto;
38 import org.sonar.db.measure.LiveMeasureDto;
39 import org.sonar.db.measure.MeasureDto;
40 import org.sonar.db.permission.GlobalPermission;
41 import org.sonar.db.project.ProjectDto;
42 import org.sonar.server.component.ComponentFinder;
43 import org.sonar.server.exceptions.BadRequestException;
44 import org.sonar.server.qualitygate.QualityGateCaycChecker;
45 import org.sonar.server.user.UserSession;
46 import org.sonar.server.ws.KeyExamples;
47 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
49 import static com.google.common.base.Strings.isNullOrEmpty;
50 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
51 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
52 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
53 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
54 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
55 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
56 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
57 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
58 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
59 import static org.sonar.server.ws.WsUtils.writeProtobuf;
61 public class ProjectStatusAction implements QualityGatesWsAction {
62 private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
64 .collect(Collectors.joining(", "));
65 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);
66 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);
68 private final DbClient dbClient;
69 private final ComponentFinder componentFinder;
70 private final UserSession userSession;
71 private final QualityGateCaycChecker qualityGateCaycChecker;
73 public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
74 this.dbClient = dbClient;
75 this.componentFinder = componentFinder;
76 this.userSession = userSession;
77 this.qualityGateCaycChecker = qualityGateCaycChecker;
81 public void define(WebService.NewController controller) {
82 WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
83 .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
85 "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
86 "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
87 "Requires one of the following permissions:" +
89 "<li>'Administer System'</li>" +
90 "<li>'Administer' rights on the specified project</li>" +
91 "<li>'Browse' on the specified project</li>" +
92 "<li>'Execute Analysis' on the specified project</li>" +
93 "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
94 .setResponseExample(getClass().getResource("project_status-example.json"))
98 new Change("9.9", "'isCaycCompliant' field is added to the response"),
99 new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
100 new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
101 new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
102 new Change("7.6", "The field 'warning' in the response is deprecated"),
103 new Change("6.4", "The field 'ignoredConditions' is added to the response"));
105 action.createParam(PARAM_ANALYSIS_ID)
106 .setDescription("Analysis id")
107 .setExampleValue(Uuids.UUID_EXAMPLE_04);
109 action.createParam(PARAM_PROJECT_ID)
111 .setDescription("Project UUID. Doesn't work with branches or pull requests")
112 .setExampleValue(Uuids.UUID_EXAMPLE_01);
114 action.createParam(PARAM_PROJECT_KEY)
116 .setDescription("Project key")
117 .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
119 action.createParam(PARAM_BRANCH)
121 .setDescription("Branch key")
122 .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
124 action.createParam(PARAM_PULL_REQUEST)
126 .setDescription("Pull request id")
127 .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
131 public void handle(Request request, Response response) throws Exception {
132 String analysisId = request.param(PARAM_ANALYSIS_ID);
133 String projectId = request.param(PARAM_PROJECT_ID);
134 String projectKey = request.param(PARAM_PROJECT_KEY);
135 String branchKey = request.param(PARAM_BRANCH);
136 String pullRequestId = request.param(PARAM_PULL_REQUEST);
138 !isNullOrEmpty(analysisId)
139 ^ !isNullOrEmpty(projectId)
140 ^ !isNullOrEmpty(projectKey),
141 MSG_ONE_PROJECT_PARAMETER_ONLY);
142 checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
144 try (DbSession dbSession = dbClient.openSession(false)) {
145 ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
146 writeProtobuf(projectStatusResponse, request, response);
150 private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
151 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
152 ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
153 checkPermission(projectAndSnapshot.project);
154 Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
155 var isCaycCompliant = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
157 return ProjectStatusResponse.newBuilder()
158 .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), isCaycCompliant).format())
162 private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
163 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
164 if (!isNullOrEmpty(analysisId)) {
165 return getSnapshotThenProject(dbSession, analysisId);
167 if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
168 return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
171 throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
174 private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
175 @Nullable String branchKey, @Nullable String pullRequestId) {
176 ProjectDto projectDto;
179 if (projectUuid != null) {
180 projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
181 branchDto = componentFinder.getMainBranch(dbSession, projectDto);
183 projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
184 branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
186 Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
187 return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
190 private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
191 SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
192 BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getComponentUuid())
193 .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
194 ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
195 .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
197 return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
200 private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
201 Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
202 return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
205 private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
207 if (!projectAndSnapshot.snapshotDto.isPresent()) {
208 return Optional.empty();
210 // get the gate status as it was computed during the specified analysis
211 String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
212 return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
213 .map(MeasureDto::getData);
216 // do not restrict to a specified analysis, use the live measure
217 Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
218 return measure.map(LiveMeasureDto::getDataAsString);
221 private void checkPermission(ProjectDto project) {
222 if (!userSession.hasProjectPermission(UserRole.ADMIN, project) &&
223 !userSession.hasProjectPermission(UserRole.USER, project) &&
224 !userSession.hasProjectPermission(UserRole.SCAN, project) &&
225 !userSession.hasPermission(GlobalPermission.SCAN)) {
226 throw insufficientPrivilegesException();
231 private static class ProjectAndSnapshot {
232 private final BranchDto branch;
233 private final Optional<SnapshotDto> snapshotDto;
234 private final ProjectDto project;
236 private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
237 this.project = project;
238 this.branch = branch;
239 this.snapshotDto = Optional.ofNullable(snapshotDto);