3 * Copyright (C) 2009-2024 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.qualitygate.QualityGateCaycStatus;
46 import org.sonar.server.user.UserSession;
47 import org.sonar.server.ws.KeyExamples;
48 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
50 import static com.google.common.base.Strings.isNullOrEmpty;
51 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
52 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
53 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
54 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
55 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
56 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
57 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
58 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
59 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
60 import static org.sonar.server.ws.WsUtils.writeProtobuf;
62 public class ProjectStatusAction implements QualityGatesWsAction {
63 private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
65 .collect(Collectors.joining(", "));
66 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);
67 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);
69 private final DbClient dbClient;
70 private final ComponentFinder componentFinder;
71 private final UserSession userSession;
72 private final QualityGateCaycChecker qualityGateCaycChecker;
74 public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
75 this.dbClient = dbClient;
76 this.componentFinder = componentFinder;
77 this.userSession = userSession;
78 this.qualityGateCaycChecker = qualityGateCaycChecker;
82 public void define(WebService.NewController controller) {
83 WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
84 .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
86 "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
87 "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
88 "Requires one of the following permissions:" +
90 "<li>'Administer System'</li>" +
91 "<li>'Administer' rights on the specified project</li>" +
92 "<li>'Browse' on the specified project</li>" +
93 "<li>'Execute Analysis' on the specified project</li>" +
94 "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
95 .setResponseExample(getClass().getResource("project_status-example.json"))
99 new Change("10.0", "The fields 'periods' and 'periodIndex' in the response are removed"),
100 new Change("9.9", "'caycStatus' field is added to the response"),
101 new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
102 new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
103 new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
104 new Change("7.6", "The field 'warning' in the response is deprecated"),
105 new Change("6.4", "The field 'ignoredConditions' is added to the response"));
107 action.createParam(PARAM_ANALYSIS_ID)
108 .setDescription("Analysis id")
109 .setExampleValue(Uuids.UUID_EXAMPLE_04);
111 action.createParam(PARAM_PROJECT_ID)
113 .setDescription("Project UUID. Doesn't work with branches or pull requests")
114 .setExampleValue(Uuids.UUID_EXAMPLE_01);
116 action.createParam(PARAM_PROJECT_KEY)
118 .setDescription("Project key")
119 .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
121 action.createParam(PARAM_BRANCH)
123 .setDescription("Branch key")
124 .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
126 action.createParam(PARAM_PULL_REQUEST)
128 .setDescription("Pull request id")
129 .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
133 public void handle(Request request, Response response) throws Exception {
134 String analysisId = request.param(PARAM_ANALYSIS_ID);
135 String projectId = request.param(PARAM_PROJECT_ID);
136 String projectKey = request.param(PARAM_PROJECT_KEY);
137 String branchKey = request.param(PARAM_BRANCH);
138 String pullRequestId = request.param(PARAM_PULL_REQUEST);
140 !isNullOrEmpty(analysisId)
141 ^ !isNullOrEmpty(projectId)
142 ^ !isNullOrEmpty(projectKey),
143 MSG_ONE_PROJECT_PARAMETER_ONLY);
144 checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
146 try (DbSession dbSession = dbClient.openSession(false)) {
147 ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
148 writeProtobuf(projectStatusResponse, request, response);
152 private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
153 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
154 ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
155 checkPermission(projectAndSnapshot.project);
156 Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
157 QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
159 return ProjectStatusResponse.newBuilder()
160 .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), caycStatus).format())
164 private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
165 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
166 if (!isNullOrEmpty(analysisId)) {
167 return getSnapshotThenProject(dbSession, analysisId);
169 if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
170 return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
173 throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
176 private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
177 @Nullable String branchKey, @Nullable String pullRequestId) {
178 ProjectDto projectDto;
181 if (projectUuid != null) {
182 projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
183 branchDto = componentFinder.getMainBranch(dbSession, projectDto);
185 projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
186 branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
188 Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
189 return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
192 private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
193 SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
194 BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getRootComponentUuid())
195 .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
196 ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
197 .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
199 return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
202 private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
203 Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
204 return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
207 private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
209 if (!projectAndSnapshot.snapshotDto.isPresent()) {
210 return Optional.empty();
212 // get the gate status as it was computed during the specified analysis
213 String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
214 return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
215 .map(MeasureDto::getData);
218 // do not restrict to a specified analysis, use the live measure
219 Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
220 return measure.map(LiveMeasureDto::getDataAsString);
223 private void checkPermission(ProjectDto project) {
224 if (!userSession.hasEntityPermission(UserRole.ADMIN, project) &&
225 !userSession.hasEntityPermission(UserRole.USER, project) &&
226 !userSession.hasEntityPermission(UserRole.SCAN, project) &&
227 !userSession.hasPermission(GlobalPermission.SCAN)) {
228 throw insufficientPrivilegesException();
233 private static class ProjectAndSnapshot {
234 private final BranchDto branch;
235 private final Optional<SnapshotDto> snapshotDto;
236 private final ProjectDto project;
238 private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
239 this.project = project;
240 this.branch = branch;
241 this.snapshotDto = Optional.ofNullable(snapshotDto);