aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-webapi
diff options
context:
space:
mode:
authorClaire Villard <claire.villard@sonarsource.com>2024-10-01 18:42:45 +0200
committersonartech <sonartech@sonarsource.com>2024-10-14 20:03:02 +0000
commitd7c1632113c8c1fe15766e3728a831a146c1de21 (patch)
tree4841a280c7c4f2c88511a31b3747decc50a350f1 /server/sonar-webserver-webapi
parentf135567f2bc832f2a24c4cf3d32d0b53d6f22cf8 (diff)
downloadsonarqube-d7c1632113c8c1fe15766e3728a831a146c1de21.tar.gz
sonarqube-d7c1632113c8c1fe15766e3728a831a146c1de21.zip
SONAR-23213 'api/system/migrate_measures' endpoint
Diffstat (limited to 'server/sonar-webserver-webapi')
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java153
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json8
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java233
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java2
5 files changed, 401 insertions, 1 deletions
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java
new file mode 100644
index 00000000000..fd7d6b35357
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.platform.ws;
+
+import com.google.common.io.Resources;
+import java.sql.SQLException;
+import java.util.List;
+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.utils.text.JsonWriter;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures;
+import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static java.util.Optional.ofNullable;
+
+public class MigrateMeasuresAction implements SystemWsAction {
+ public static final String SYSTEM_MEASURES_MIGRATION_ENABLED = "system.measures.migration.enabled";
+ public static final String PARAM_SIZE = "size";
+
+ private final UserSession userSession;
+ private final DbClient dbClient;
+ private final MigrateBranchesLiveMeasuresToMeasures branchesMigration;
+ private final MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration;
+
+ public MigrateMeasuresAction(UserSession userSession, DbClient dbClient,
+ MigrateBranchesLiveMeasuresToMeasures branchesMigration, MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration) {
+ this.userSession = userSession;
+ this.dbClient = dbClient;
+ this.branchesMigration = branchesMigration;
+ this.portfoliosMigration = portfoliosMigration;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ WebService.NewAction action = controller.createAction("migrate_measures")
+ .setDescription("Prepare the migration to the next major version of SonarQube." +
+ "<br/>" +
+ "Sending a POST request to this URL will migrate some rows from the 'live_measures' to the 'measures' table. " +
+ "Requires system administration permission.")
+ .setSince("9.9.8")
+ .setPost(true)
+ .setHandler(this)
+ .setInternal(true)
+ .setResponseExample(Resources.getResource(this.getClass(), "example-migrate_measures.json"));
+
+ action.createParam(PARAM_SIZE)
+ .setDescription("The number of branches or portfolios to migrate")
+ .setDefaultValue(10);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ userSession.checkIsSystemAdministrator();
+
+ if (!isMigrationEnabled()) {
+ throw new IllegalStateException("Migration is not enabled. Please call the endpoint /api/system/prepare_migration?enable=true and retry.");
+ }
+
+ int size = request.mandatoryParamAsInt(PARAM_SIZE);
+ if (size <= 0) {
+ throw new IllegalArgumentException("Size must be greater than 0");
+ }
+
+ int migratedItems = migrateBranches(size);
+ if (migratedItems < size) {
+ int remainingSize = size - migratedItems;
+ migratedItems += migratePortfolios(remainingSize);
+ }
+
+ BranchStats statistics = getStatistics();
+ try (JsonWriter json = response.newJsonWriter()) {
+ json.beginObject()
+ .prop("status", "success")
+ .prop("message", format("%s branches or portfolios migrated", migratedItems))
+ .prop("remainingBranches", statistics.remainingBranches)
+ .prop("totalBranches", statistics.totalBranches)
+ .prop("remainingPortfolios", statistics.remainingPortfolios)
+ .prop("totalPortfolios", statistics.totalPortfolios)
+ .endObject();
+ }
+ }
+
+ private int migrateBranches(int size) throws SQLException {
+ List<String> branchesToMigrate = getBranchesToMigrate(size);
+ if (!branchesToMigrate.isEmpty()) {
+ branchesMigration.migrate(branchesToMigrate);
+ }
+ return branchesToMigrate.size();
+ }
+
+ private List<String> getBranchesToMigrate(int size) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return dbClient.branchDao().selectUuidsWithMeasuresMigratedFalse(dbSession, size);
+ }
+ }
+
+ private int migratePortfolios(int size) throws SQLException {
+ List<String> portfoliosToMigrate = getPortfoliosToMigrate(size);
+ if (!portfoliosToMigrate.isEmpty()) {
+ portfoliosMigration.migrate(portfoliosToMigrate);
+ }
+ return portfoliosToMigrate.size();
+ }
+
+ private List<String> getPortfoliosToMigrate(int size) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return dbClient.portfolioDao().selectUuidsWithMeasuresMigratedFalse(dbSession, size);
+ }
+ }
+
+ private boolean isMigrationEnabled() {
+ return ofNullable(dbClient.propertiesDao().selectGlobalProperty(SYSTEM_MEASURES_MIGRATION_ENABLED))
+ .map(p -> Boolean.parseBoolean(p.getValue()))
+ .orElse(false);
+ }
+
+ private BranchStats getStatistics() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ int remainingBranches = dbClient.branchDao().countByMeasuresMigratedFalse(dbSession);
+ int totalBranches = dbClient.branchDao().countAll(dbSession);
+ int remainingPortfolios = dbClient.portfolioDao().countByMeasuresMigratedFalse(dbSession);
+ int totalPortfolios = dbClient.portfolioDao().selectAll(dbSession).size();
+
+ return new BranchStats(remainingBranches, totalBranches, remainingPortfolios, totalPortfolios);
+ }
+ }
+
+ private record BranchStats(int remainingBranches, int totalBranches, int remainingPortfolios, int totalPortfolios) {
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java
index 5807d697a88..41c21b03174 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java
@@ -25,6 +25,8 @@ import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToP
import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated;
import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated;
import org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable;
+import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures;
+import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures;
public class SystemWsModule extends Module {
@@ -47,6 +49,10 @@ public class SystemWsModule extends Module {
CreateIndexOnPortfoliosMeasuresMigrated.class,
PrepareMigrationAction.class,
+ MigrateBranchesLiveMeasuresToMeasures.class,
+ MigratePortfoliosLiveMeasuresToMeasures.class,
+ MigrateMeasuresAction.class,
+
InfoAction.class,
LogsAction.class,
MigrateDbAction.class,
diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json
new file mode 100644
index 00000000000..60fb3154d7a
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json
@@ -0,0 +1,8 @@
+{
+ "status": "success",
+ "message": "2 branches or portfolios migrated",
+ "remainingBranches": 1,
+ "totalBranches": 3,
+ "remainingPortfolios": 1,
+ "totalPortfolios": 2
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java
new file mode 100644
index 00000000000..a4b92076fca
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java
@@ -0,0 +1,233 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.platform.ws;
+
+import com.google.gson.Gson;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import java.sql.SQLException;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.db.Database;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.portfolio.PortfolioDto;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable;
+import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures;
+import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures;
+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.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.sonar.db.component.BranchType.BRANCH;
+import static org.sonar.server.platform.ws.MigrateMeasuresAction.SYSTEM_MEASURES_MIGRATION_ENABLED;
+import static org.sonar.test.JsonAssert.assertJson;
+
+@RunWith(DataProviderRunner.class)
+public class MigrateMeasuresActionTest {
+ private static final Gson GSON = new Gson();
+
+ public static final String PARAM_SIZE = "size";
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone().logIn().setSystemAdministrator();
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ private final MigrateBranchesLiveMeasuresToMeasures measuresMigration = mock();
+ private final MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration = mock();
+ private final MigrateMeasuresAction underTest = new MigrateMeasuresAction(userSessionRule, dbTester.getDbClient(), measuresMigration, portfoliosMigration);
+ private final WsActionTester tester = new WsActionTester(underTest);
+
+ @Test
+ public void should_throw_if_migration_is_not_enabled() {
+ TestRequest request = tester.newRequest();
+
+ assertThatIllegalStateException()
+ .isThrownBy(request::execute)
+ .withMessage("Migration is not enabled. Please call the endpoint /api/system/prepare_migration?enable=true and retry.");
+ }
+
+ @Test
+ @DataProvider(value = {"0", "-1", "-100"})
+ public void should_throws_IAE_if_size_in_invalid(int size) throws SQLException {
+ enableMigration();
+
+ TestRequest request = tester
+ .newRequest()
+ .setParam(PARAM_SIZE, Integer.toString(size));
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(request::execute)
+ .withMessage("Size must be greater than 0");
+ }
+
+ @Test
+ public void verify_example() throws SQLException {
+ enableMigration();
+ // 3 branches, 2 migrated
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH));
+ dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH));
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true);
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true);
+ // 2 portfolios, 1 migrated
+ PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1");
+ dbTester.components().insertPrivatePortfolioDto("name2");
+ dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true);
+ dbTester.getSession().commit();
+
+ TestResponse response = tester.newRequest()
+ .execute();
+
+ assertJson(response.getInput()).isSimilarTo(getClass().getResource("example-migrate_measures.json"));
+ }
+
+ @Test
+ public void does_not_migrate_portfolios_if_measures_are_not_finished() throws SQLException {
+ enableMigration();
+ // 2 branches
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ ComponentDto branch = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH));
+ dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH));
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_SIZE, "2")
+ .execute();
+
+ assertThat(GSON.fromJson(response.getInput(), ActionResponse.class))
+ .isEqualTo(new ActionResponse("success", "2 branches or portfolios migrated", 3, 3, 0, 0));
+ verify(measuresMigration).migrate(List.of(project.uuid(), branch.uuid()));
+ verifyNoInteractions(portfoliosMigration);
+ }
+
+ @Test
+ public void migrate_portfolios_to_reach_the_requested_size() throws SQLException {
+ enableMigration();
+
+ // 1 branch
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ // 2 portfolios
+ PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1");
+ dbTester.components().insertPrivatePortfolioDto("name2");
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_SIZE, "2")
+ .execute();
+
+ assertThat(GSON.fromJson(response.getInput(), ActionResponse.class))
+ .isEqualTo(new ActionResponse("success", "2 branches or portfolios migrated", 1, 1, 2, 2));
+ verify(measuresMigration).migrate(List.of(project.uuid()));
+ verify(portfoliosMigration).migrate(List.of(portfolio1.getUuid()));
+ }
+
+ @Test
+ public void migrate_portfolios_only_if_measures_are_done() throws SQLException {
+ enableMigration();
+ // 2 branches, all migrated
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH));
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true);
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true);
+ // 2 portfolios, 1 migrated
+ PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1");
+ PortfolioDto portfolio2 = dbTester.components().insertPrivatePortfolioDto("name2");
+ dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true);
+ dbTester.commit();
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_SIZE, "2")
+ .execute();
+
+ assertThat(GSON.fromJson(response.getInput(), ActionResponse.class))
+ .isEqualTo(new ActionResponse("success", "1 branches or portfolios migrated", 0, 2, 1, 2));
+ verifyNoInteractions(measuresMigration);
+ verify(portfoliosMigration).migrate(List.of(portfolio2.getUuid()));
+ }
+
+ @Test
+ public void does_nothing_if_migration_is_finished() throws SQLException {
+ enableMigration();
+ // 2 branches, all migrated
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH));
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true);
+ dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true);
+ // 2 portfolios, all migrated
+ PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1");
+ PortfolioDto portfolio2 = dbTester.components().insertPrivatePortfolioDto("name2");
+ dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true);
+ dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio2.getUuid(), true);
+ dbTester.commit();
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_SIZE, "2")
+ .execute();
+
+ assertThat(GSON.fromJson(response.getInput(), ActionResponse.class))
+ .isEqualTo(new ActionResponse("success", "0 branches or portfolios migrated", 0, 2, 0, 2));
+ verifyNoInteractions(measuresMigration, portfoliosMigration);
+ }
+
+ private void enableMigration() throws SQLException {
+ Database database = dbTester.getDbClient().getDatabase();
+ new AddMeasuresMigratedColumnToProjectBranchesTable(database).execute();
+ new AddMeasuresMigratedColumnToPortfoliosTable(database).execute();
+ dbTester.getDbClient().propertiesDao().saveProperty(new PropertyDto().setKey(SYSTEM_MEASURES_MIGRATION_ENABLED).setValue("true"));
+ }
+
+ @Test
+ public void throws_ForbiddenException_if_user_is_not_logged_in() {
+ userSessionRule.anonymous();
+
+ TestRequest request = tester.newRequest();
+
+ assertThatExceptionOfType(ForbiddenException.class)
+ .isThrownBy(request::execute);
+ }
+
+ @Test
+ public void throws_ForbiddenException_if_user_is_not_system_admin() {
+ userSessionRule.logIn();
+
+ TestRequest request = tester.newRequest();
+
+ assertThatExceptionOfType(ForbiddenException.class)
+ .isThrownBy(request::execute);
+ }
+
+ private record ActionResponse(String status, String message, int remainingBranches, int totalBranches, int remainingPortfolios,
+ int totalPortfolios) {
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java
index 1249de66c19..8c7a40815c2 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java
@@ -29,6 +29,6 @@ public class SystemWsModuleTest {
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new SystemWsModule().configure(container);
- assertThat(container.getAddedObjects()).hasSize(21);
+ assertThat(container.getAddedObjects()).hasSize(24);
}
}