aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-webapi-v2
diff options
context:
space:
mode:
authorlukasz-jarocki-sonarsource <lukasz.jarocki@sonarsource.com>2024-04-29 12:22:39 +0200
committersonartech <sonartech@sonarsource.com>2024-05-08 20:02:44 +0000
commit80942bfb9760881f2c26969c885d370fd4abd92e (patch)
tree52e0c3fe5bda97022a3f44bf68ece25a31053535 /server/sonar-webserver-webapi-v2
parent56983ba3969f653d3939484ce843a3ce928840a9 (diff)
downloadsonarqube-80942bfb9760881f2c26969c885d370fd4abd92e.tar.gz
sonarqube-80942bfb9760881f2c26969c885d370fd4abd92e.zip
SONAR-22141 added new endpoint /api/v2/system/migrations-status
Diffstat (limited to 'server/sonar-webserver-webapi-v2')
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java86
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DateString.java37
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java10
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java10
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java152
6 files changed, 296 insertions, 0 deletions
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
index b1abd6013b4..a4e820e1be5 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
@@ -23,6 +23,7 @@ public class WebApiEndpoints {
private static final String SYSTEM_ENDPOINTS = "/system";
public static final String LIVENESS_ENDPOINT = SYSTEM_ENDPOINTS + "/liveness";
public static final String HEALTH_ENDPOINT = SYSTEM_ENDPOINTS + "/health";
+ public static final String DATABASE_MIGRATIONS_ENDPOINT = SYSTEM_ENDPOINTS + "/migrations-status";
public static final String USERS_MANAGEMENT_DOMAIN = "/users-management";
public static final String USER_ENDPOINT = USERS_MANAGEMENT_DOMAIN + "/users";
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java
new file mode 100644
index 00000000000..d585473bc56
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java
@@ -0,0 +1,86 @@
+/*
+ * 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.v2.api.system.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.DatabaseMigrationState;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+import org.sonar.server.v2.common.DateString;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.sonar.server.platform.db.migration.DatabaseMigrationState.NO_CONNECTION_TO_DB;
+import static org.sonar.server.platform.db.migration.DatabaseMigrationState.UNSUPPORTED_DATABASE_MIGRATION_STATUS;
+import static org.sonar.server.v2.WebApiEndpoints.DATABASE_MIGRATIONS_ENDPOINT;
+
+@RestController
+@RequestMapping(DATABASE_MIGRATIONS_ENDPOINT)
+public class DatabaseMigrationsController {
+
+ private final DatabaseVersion databaseVersion;
+ private final DatabaseMigrationState databaseMigrationState;
+ private final Database database;
+
+ public DatabaseMigrationsController(DatabaseVersion databaseVersion, DatabaseMigrationState databaseMigrationState, Database database) {
+ this.databaseVersion = databaseVersion;
+ this.databaseMigrationState = databaseMigrationState;
+ this.database = database;
+ }
+
+ @Operation(summary = "Gets the status of ongoing database migrations, if any", description = "Return the detailed status of ongoing database migrations" +
+ " including starting date. If no migration is ongoing or needed it is still possible to call this endpoint and receive appropriate information.")
+ @GetMapping
+ public DatabaseMigrationsResponse getStatus() {
+ Optional<Long> currentVersion = databaseVersion.getVersion();
+ checkState(currentVersion.isPresent(), NO_CONNECTION_TO_DB);
+ DatabaseVersion.Status status = databaseVersion.getStatus();
+ if (status == DatabaseVersion.Status.UP_TO_DATE || status == DatabaseVersion.Status.REQUIRES_DOWNGRADE) {
+ return new DatabaseMigrationsResponse(databaseMigrationState);
+ } else if (!database.getDialect().supportsMigration()) {
+ return new DatabaseMigrationsResponse(DatabaseMigrationState.Status.STATUS_NOT_SUPPORTED);
+ } else {
+ return switch (databaseMigrationState.getStatus()) {
+ case RUNNING, FAILED, SUCCEEDED -> new DatabaseMigrationsResponse(databaseMigrationState);
+ case NONE -> new DatabaseMigrationsResponse(DatabaseMigrationState.Status.MIGRATION_REQUIRED);
+ default -> throw new IllegalArgumentException(UNSUPPORTED_DATABASE_MIGRATION_STATUS);
+ };
+ }
+
+ }
+
+ public record DatabaseMigrationsResponse(String status, @Nullable String startedAt, @Nullable String message) {
+
+ public DatabaseMigrationsResponse(DatabaseMigrationState state) {
+ this(state.getStatus().toString(),
+ DateString.from(state.getStartedAt()),
+ state.getError() != null ? state.getError().getMessage() : state.getStatus().getMessage());
+ }
+
+ public DatabaseMigrationsResponse(DatabaseMigrationState.Status status) {
+ this(status.toString(), null, status.getMessage());
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DateString.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DateString.java
new file mode 100644
index 00000000000..1f7cbd6c483
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DateString.java
@@ -0,0 +1,37 @@
+/*
+ * 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.v2.common;
+
+import java.util.Date;
+import javax.annotation.Nullable;
+
+public class DateString {
+
+ private DateString() {
+ // intentionally empty
+ }
+
+ public static String from(@Nullable Date date) {
+ if (date == null) {
+ return null;
+ }
+ return date.toString();
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
index d63cef69138..7ec4a6387b8 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
@@ -22,6 +22,7 @@ package org.sonar.server.v2.config;
import javax.annotation.Nullable;
import org.sonar.api.platform.Server;
import org.sonar.api.resources.Languages;
+import org.sonar.db.Database;
import org.sonar.db.DbClient;
import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
import org.sonar.server.common.group.service.GroupMembershipService;
@@ -41,6 +42,8 @@ import org.sonar.server.common.user.service.UserService;
import org.sonar.server.health.HealthChecker;
import org.sonar.server.platform.NodeInformation;
import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.server.platform.db.migration.DatabaseMigrationState;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;
@@ -69,6 +72,7 @@ import org.sonar.server.v2.api.projects.controller.DefaultBoundProjectsControlle
import org.sonar.server.v2.api.rule.controller.DefaultRuleController;
import org.sonar.server.v2.api.rule.controller.RuleController;
import org.sonar.server.v2.api.rule.converter.RuleRestResponseGenerator;
+import org.sonar.server.v2.api.system.controller.DatabaseMigrationsController;
import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
import org.sonar.server.v2.api.system.controller.HealthController;
import org.sonar.server.v2.api.system.controller.LivenessController;
@@ -103,6 +107,12 @@ public class PlatformLevel4WebConfig {
}
@Bean
+ public DatabaseMigrationsController databaseMigrationsController(DatabaseVersion databaseVersion, DatabaseMigrationState databaseMigrationState,
+ Database database) {
+ return new DatabaseMigrationsController(databaseVersion, databaseMigrationState, database);
+ }
+
+ @Bean
public UsersSearchRestResponseGenerator usersSearchResponseGenerator(UserSession userSession) {
return new UsersSearchRestResponseGenerator(userSession);
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
index 04d43567764..f235bd848b3 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
@@ -19,11 +19,15 @@
*/
package org.sonar.server.v2.config;
+import org.sonar.db.Database;
import org.sonar.server.common.health.DbConnectionNodeCheck;
import org.sonar.server.common.platform.LivenessChecker;
import org.sonar.server.common.platform.SafeModeLivenessCheckerImpl;
import org.sonar.server.health.HealthChecker;
+import org.sonar.server.platform.db.migration.DatabaseMigrationState;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.api.system.controller.DatabaseMigrationsController;
import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
import org.sonar.server.v2.api.system.controller.HealthController;
import org.sonar.server.v2.api.system.controller.LivenessController;
@@ -51,4 +55,10 @@ public class SafeModeWebConfig {
public HealthController healthController(HealthChecker healthChecker, SystemPasscode systemPasscode) {
return new HealthController(healthChecker, systemPasscode);
}
+
+ @Bean
+ public DatabaseMigrationsController databaseMigrationsController(DatabaseVersion databaseVersion, DatabaseMigrationState databaseMigrationState,
+ Database database) {
+ return new DatabaseMigrationsController(databaseVersion, databaseMigrationState, database);
+ }
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java
new file mode 100644
index 00000000000..5b2bc2d5f05
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.v2.api.system.controller;
+
+import java.util.Date;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.sonar.db.Database;
+import org.sonar.db.dialect.Dialect;
+import org.sonar.server.platform.db.migration.DatabaseMigrationState;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.platform.db.migration.DatabaseMigrationState.Status.FAILED;
+import static org.sonar.server.platform.db.migration.DatabaseMigrationState.Status.NONE;
+import static org.sonar.server.platform.db.migration.DatabaseMigrationState.Status.RUNNING;
+import static org.sonar.server.v2.WebApiEndpoints.DATABASE_MIGRATIONS_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class DatabaseMigrationsControllerTest {
+
+ private static final Date SOME_DATE = new Date();
+ private final DatabaseVersion databaseVersion = mock();
+ private final DatabaseMigrationState migrationState = mock();
+ private final Dialect dialect = mock(Dialect.class);
+ private final Database database = mock();
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new DatabaseMigrationsController(databaseVersion, migrationState, database));
+
+ @BeforeEach
+ public void before() {
+ when(database.getDialect()).thenReturn(dialect);
+ when(databaseVersion.getVersion()).thenReturn(Optional.of(1L));
+ }
+
+ @Test
+ void getStatus_whenDatabaseHasNoVersion_return500() throws Exception {
+ Mockito.reset(databaseVersion);
+ when(databaseVersion.getVersion()).thenReturn(Optional.empty());
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().is5xxServerError(),
+ content().json("{\"message\":\"Cannot connect to Database.\"}"));
+ }
+
+ @Test
+ void getStatus_migrationNotNeeded_returnUpToDateStatus() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.UP_TO_DATE);
+ when(migrationState.getStatus()).thenReturn(NONE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"NO_MIGRATION\",\"message\":\"Database is up-to-date, no migration needed.\"}"));
+ }
+
+ @Test
+ void getStatus_whenDowngradeRequired_returnNone() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_DOWNGRADE);
+ when(migrationState.getStatus()).thenReturn(NONE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"NO_MIGRATION\",\"message\":\"Database is up-to-date, no migration needed.\"}"));
+ }
+
+ @Test
+ void getStatus_whenDbRequiresUpgradeButDialectIsNotSupported_returnNotSupported() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.FRESH_INSTALL);
+ when(dialect.supportsMigration()).thenReturn(false);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"NOT_SUPPORTED\",\"message\":\"Upgrade is not supported on embedded database.\"}"));
+ }
+
+ @Test
+ void getStatus_whenDbMigrationsRunning_returnRunning() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(dialect.supportsMigration()).thenReturn(true);
+ when(migrationState.getStatus()).thenReturn(RUNNING);
+ when(migrationState.getStartedAt()).thenReturn(SOME_DATE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"MIGRATION_RUNNING\",\"message\":\"Database migration is running.\"}"));
+ }
+
+ @Test
+ void getStatus_whenDbMigrationsFailed_returnFailed() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(dialect.supportsMigration()).thenReturn(true);
+ when(migrationState.getStatus()).thenReturn(DatabaseMigrationState.Status.FAILED);
+ when(migrationState.getStartedAt()).thenReturn(SOME_DATE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"MIGRATION_FAILED\",\"message\":\"Migration failed: %s.<br/> Please check logs.\"}"));
+ }
+
+ @Test
+ void getStatus_whenDbMigrationsSucceeded_returnSucceeded() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(dialect.supportsMigration()).thenReturn(true);
+ when(migrationState.getStatus()).thenReturn(DatabaseMigrationState.Status.SUCCEEDED);
+ when(migrationState.getStartedAt()).thenReturn(SOME_DATE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"MIGRATION_SUCCEEDED\",\"message\":\"Migration succeeded.\"}"));
+ }
+
+
+ @Test
+ void getStatus_whenMigrationRequired_returnMigrationRequired() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(dialect.supportsMigration()).thenReturn(true);
+ when(migrationState.getStatus()).thenReturn(NONE);
+ when(migrationState.getStartedAt()).thenReturn(SOME_DATE);
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"MIGRATION_REQUIRED\",\"message\":\"Database migration is required. DB migration " +
+ "can be started using WS /api/system/migrate_db.\"}"));
+ }
+
+ @Test
+ void getStatus_whenMigrationFailedWithError_IncludeErrorInResponse() throws Exception {
+ when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.FRESH_INSTALL);
+ when(dialect.supportsMigration()).thenReturn(true);
+ when(migrationState.getStatus()).thenReturn(FAILED);
+ when(migrationState.getStartedAt()).thenReturn(SOME_DATE);
+ when(migrationState.getError()).thenReturn(new UnsupportedOperationException("error message"));
+
+ mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(),
+ content().json("{\"status\":\"MIGRATION_FAILED\",\"message\":\"error message\"}"));
+ }
+}