diff options
Diffstat (limited to 'server')
20 files changed, 552 insertions, 6 deletions
diff --git a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java index 3bfa5184256..40981cd7d13 100644 --- a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java +++ b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java @@ -108,6 +108,7 @@ public final class SqTables { "rules_parameters", "rules_profiles", "rule_repositories", + "sca_analyses", "sca_dependencies", "sca_encountered_licenses", "sca_issues", diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java index e1fb868b7c0..a7bf75927ec 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java @@ -2001,6 +2001,21 @@ oldCreationDate)); "sca_issue_uuid", "issue-uuid2", "sca_release_uuid", "release-uuid2"))); assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(2); + + var analysisBase = Map.of( + "created_at", 0L, + "updated_at", 0L, + "status", "COMPLETED", + "errors", "[]", + "parsed_files", "[]", + "failed_reason", "something"); + db.executeInsert("sca_analyses", merge(analysisBase, Map.of( + "uuid", "analysis-uuid1", + "component_uuid", branch1Uuid))); + db.executeInsert("sca_analyses", merge(analysisBase, Map.of( + "uuid", "analysis-uuid2", + "component_uuid", branch2Uuid))); + assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(2); } @Test @@ -2016,6 +2031,48 @@ oldCreationDate)); assertThat(db.countRowsOfTable(dbSession, "sca_releases")).isEqualTo(1); assertThat(db.countRowsOfTable(dbSession, "sca_dependencies")).isEqualTo(1); assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(1); + assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(1); + } + + @Test + void deleteProject_purgesScaLicenseProfiles() { + ProjectDto project = db.components().insertPublicProject().getProjectDto(); + + var scaLicenseProfileProjectBase = Map.of( + "sca_license_profile_uuid", "sca-license-profile-uuid1", + "created_at", 0L, + "updated_at", 0L); + + db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of( + "uuid", "sca-lic-prof-project-uuid1", + "project_uuid", project.getUuid()))); + + db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of( + "uuid", "sca-lic-prof-project-uuid2", + "project_uuid", "other-project-uuid"))); + + assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(2); + + underTest.deleteProject(dbSession, project.getUuid(), project.getQualifier(), project.getName(), project.getKey()); + + assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(1); + } + + @Test + void whenDeleteBranch_thenPurgeArchitectureGraphs() { + ProjectDto project = db.components().insertPublicProject().getProjectDto(); + BranchDto branch1 = db.components().insertProjectBranch(project); + BranchDto branch2 = db.components().insertProjectBranch(project); + + db.executeInsert("architecture_graphs", Map.of("uuid", "12345", "branch_uuid", branch1.getUuid(), "source", "xoo", "type", "file_graph", "graph_data", "{}")); + db.executeInsert("architecture_graphs", Map.of("uuid", "123456", "branch_uuid", branch1.getUuid(), "source", "xoo", "type", "class_graph", "graph_data", "{}")); + db.executeInsert("architecture_graphs", Map.of("uuid", "1234567", "branch_uuid", branch2.getUuid(), "source", "xoo", "type", "file_graph", "graph_data", "{}")); + + assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(3); + underTest.deleteBranch(dbSession, branch1.getUuid()); + assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(1); + underTest.deleteBranch(dbSession, branch2.getUuid()); + assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isZero(); } private AnticipatedTransitionDto getAnticipatedTransitionsDto(String uuid, String projectUuid, Date creationDate) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java index 230d9aff010..d818e2361cf 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java @@ -510,6 +510,13 @@ class PurgeCommands { profiler.stop(); } + public void deleteArchitectureGraphs(String branchUuid) { + profiler.start("deleteArchitectureGraphs (architecture_graphs)"); + purgeMapper.deleteArchitectureGraphsByBranchUuid(branchUuid); + session.commit(); + profiler.stop(); + } + public void deleteAnticipatedTransitions(String projectUuid, long createdAt) { profiler.start("deleteAnticipatedTransitions (anticipated_transitions)"); purgeMapper.deleteAnticipatedTransitionsByProjectUuidAndCreationDate(projectUuid, createdAt); @@ -525,6 +532,12 @@ class PurgeCommands { } public void deleteScaActivity(String componentUuid) { + // delete sca_analyses first since it sort of marks the analysis as valid/existing + profiler.start("deleteScaAnalyses (sca_analyses)"); + purgeMapper.deleteScaAnalysesByComponentUuid(componentUuid); + session.commit(); + profiler.stop(); + profiler.start("deleteScaDependencies (sca_dependencies)"); purgeMapper.deleteScaDependenciesByComponentUuid(componentUuid); session.commit(); @@ -542,4 +555,10 @@ class PurgeCommands { session.commit(); profiler.stop(); } + + public void deleteScaLicenseProfiles(String projectUuid) { + profiler.start("deleteScaLicenseProfileProjects (sca_lic_prof_projects)"); + purgeMapper.deleteScaLicenseProfileProjectsByProjectUuid(projectUuid); + profiler.stop(); + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java index ce5e0cf5e70..ff34ce5783d 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java @@ -281,6 +281,7 @@ public class PurgeDao implements Dao { commands.deleteReportSubscriptions(branchUuid); commands.deleteIssuesFixed(branchUuid); commands.deleteScaActivity(branchUuid); + commands.deleteArchitectureGraphs(branchUuid); } private static void deleteProject(String projectUuid, PurgeMapper mapper, PurgeCommands commands) { @@ -313,6 +314,7 @@ public class PurgeDao implements Dao { commands.deleteOutdatedProperties(projectUuid); commands.deleteReportSchedules(projectUuid); commands.deleteReportSubscriptions(projectUuid); + commands.deleteScaLicenseProfiles(projectUuid); } /** diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java index 5ca08a12d7a..2502ac8e5d8 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java @@ -195,9 +195,15 @@ public interface PurgeMapper { void deleteIssuesFixedByBranchUuid(@Param("branchUuid") String branchUuid); + void deleteScaAnalysesByComponentUuid(@Param("componentUuid") String componentUuid); + void deleteScaDependenciesByComponentUuid(@Param("componentUuid") String componentUuid); void deleteScaIssuesReleasesByComponentUuid(@Param("componentUuid") String componentUuid); void deleteScaReleasesByComponentUuid(@Param("componentUuid") String componentUuid); + + void deleteArchitectureGraphsByBranchUuid(@Param("branchUuid") String branchUuid); + + void deleteScaLicenseProfileProjectsByProjectUuid(@Param("projectUuid") String projectUuid); } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml index 4a64f3cdeab..9b0b865fc36 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml @@ -670,7 +670,13 @@ delete from issues_fixed where pull_request_uuid = #{branchUuid,jdbcType=VARCHAR} </delete> + <delete id="deleteArchitectureGraphsByBranchUuid"> + delete from architecture_graphs where branch_uuid = #{branchUuid,jdbcType=VARCHAR} + </delete> + <delete id="deleteScaAnalysesByComponentUuid"> + delete from sca_analyses where component_uuid = #{componentUuid,jdbcType=VARCHAR} + </delete> <delete id="deleteScaDependenciesByComponentUuid"> delete from sca_dependencies where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR}) </delete> @@ -680,4 +686,7 @@ <delete id="deleteScaReleasesByComponentUuid"> delete from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR} </delete> + <delete id="deleteScaLicenseProfileProjectsByProjectUuid"> + delete from sca_lic_prof_projects where project_uuid = #{projectUuid,jdbcType=VARCHAR} + </delete> </mapper> diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index bb7e4166b03..1f942ae9125 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -1042,6 +1042,19 @@ CREATE TABLE "SAML_MESSAGE_IDS"( ALTER TABLE "SAML_MESSAGE_IDS" ADD CONSTRAINT "PK_SAML_MESSAGE_IDS" PRIMARY KEY("UUID"); CREATE UNIQUE NULLS NOT DISTINCT INDEX "SAML_MESSAGE_IDS_UNIQUE" ON "SAML_MESSAGE_IDS"("MESSAGE_ID" NULLS FIRST); +CREATE TABLE "SCA_ANALYSES"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL, + "STATUS" CHARACTER VARYING(40) NOT NULL, + "FAILED_REASON" CHARACTER VARYING(255), + "ERRORS" CHARACTER LARGE OBJECT NOT NULL, + "PARSED_FILES" CHARACTER LARGE OBJECT NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "SCA_ANALYSES" ADD CONSTRAINT "PK_SCA_ANALYSES" PRIMARY KEY("UUID"); +CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_ANALYSES_COMPONENT_UNIQ" ON "SCA_ANALYSES"("COMPONENT_UUID" NULLS FIRST); + CREATE TABLE "SCA_DEPENDENCIES"( "UUID" CHARACTER VARYING(40) NOT NULL, "SCA_RELEASE_UUID" CHARACTER VARYING(40) NOT NULL, diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java new file mode 100644 index 00000000000..f67d12fb20c --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.db.migration.version.v202503; + +import java.sql.SQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static java.sql.Types.BIGINT; +import static java.sql.Types.CLOB; +import static java.sql.Types.VARCHAR; +import static org.sonar.db.MigrationDbTester.createForMigrationStep; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE; + +class CreateScaAnalysesTableIT { + private static final String TABLE_NAME = "sca_analyses"; + + @RegisterExtension + public final MigrationDbTester db = createForMigrationStep(CreateScaAnalysesTable.class); + private final DdlChange underTest = new CreateScaAnalysesTable(db.database()); + + @Test + void execute_shouldCreateTable() throws SQLException { + db.assertTableDoesNotExist(TABLE_NAME); + underTest.execute(); + db.assertTableExists(TABLE_NAME); + db.assertPrimaryKey(TABLE_NAME, "pk_sca_analyses", "uuid"); + db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false); + db.assertColumnDefinition(TABLE_NAME, "component_uuid", VARCHAR, UUID_SIZE, false); + db.assertColumnDefinition(TABLE_NAME, "status", VARCHAR, 40, false); + db.assertColumnDefinition(TABLE_NAME, "failed_reason", VARCHAR, 255, true); + db.assertColumnDefinition(TABLE_NAME, "errors", CLOB, null, false); + db.assertColumnDefinition(TABLE_NAME, "parsed_files", CLOB, null, false); + db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false); + db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false); + } + + @Test + void execute_shouldBeReentrant() throws SQLException { + db.assertTableDoesNotExist(TABLE_NAME); + underTest.execute(); + underTest.execute(); + db.assertTableExists(TABLE_NAME); + } +} diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java new file mode 100644 index 00000000000..7ccc34ec67e --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.db.migration.version.v202503; + +import java.sql.SQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.db.MigrationDbTester.createForMigrationStep; +import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.COLUMN_NAME_COMPONENT_UUID; +import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.INDEX_NAME; +import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.TABLE_NAME; + +class CreateUniqueIndexOnScaAnalysesIT { + @RegisterExtension + public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaAnalyses.class); + private final DdlChange underTest = new CreateUniqueIndexOnScaAnalyses(db.database()); + + @Test + void execute_shouldCreateIndex() throws SQLException { + db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME); + underTest.execute(); + db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID); + } + + @Test + void execute_shouldBeReentrant() throws SQLException { + db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME); + underTest.execute(); + underTest.execute(); + db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java new file mode 100644 index 00000000000..1356126259a --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.db.migration.version.v202503; + +import java.sql.SQLException; +import java.util.List; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.sql.CreateTableBuilder; +import org.sonar.server.platform.db.migration.step.CreateTableChange; + +import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder; +import static org.sonar.server.platform.db.migration.def.ClobColumnDef.newClobColumnDefBuilder; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder; + +public class CreateScaAnalysesTable extends CreateTableChange { + public static final String TABLE_NAME = "sca_analyses"; + public static final int STATUS_COLUMN_SIZE = 40; + public static final int FAILED_REASON_COLUMN_SIZE = 255; + + protected CreateScaAnalysesTable(Database db) { + super(db, TABLE_NAME); + } + + @Override + public void execute(Context context, String tableName) throws SQLException { + List<String> createQuery = new CreateTableBuilder(getDialect(), tableName) + .addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build()) + .addColumn(newVarcharColumnDefBuilder().setColumnName("component_uuid").setIsNullable(false).setLimit(UUID_SIZE).build()) + .addColumn(newVarcharColumnDefBuilder().setColumnName("status").setIsNullable(false).setLimit(STATUS_COLUMN_SIZE).build()) + .addColumn(newVarcharColumnDefBuilder().setColumnName("failed_reason").setIsNullable(true).setLimit(FAILED_REASON_COLUMN_SIZE).build()) + .addColumn(newClobColumnDefBuilder().setColumnName("errors").setIsNullable(false).build()) + .addColumn(newClobColumnDefBuilder().setColumnName("parsed_files").setIsNullable(false).build()) + .addColumn(newBigIntegerColumnDefBuilder().setColumnName("created_at").setIsNullable(false).build()) + .addColumn(newBigIntegerColumnDefBuilder().setColumnName("updated_at").setIsNullable(false).build()) + .build(); + + context.execute(createQuery); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java new file mode 100644 index 00000000000..0a82ad81e4b --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.db.migration.version.v202503; + +import java.sql.Connection; +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.db.DatabaseUtils; +import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class CreateUniqueIndexOnScaAnalyses extends DdlChange { + static final String TABLE_NAME = "sca_analyses"; + static final String INDEX_NAME = "sca_analyses_component_uniq"; + static final String COLUMN_NAME_COMPONENT_UUID = "component_uuid"; + + public CreateUniqueIndexOnScaAnalyses(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + try (Connection connection = getDatabase().getDataSource().getConnection()) { + createIndex(context, connection); + } + } + + private void createIndex(Context context, Connection connection) { + if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) { + context.execute(new CreateIndexBuilder(getDialect()) + .setTable(TABLE_NAME) + .setName(INDEX_NAME) + .setUnique(true) + .addColumn(COLUMN_NAME_COMPONENT_UUID, false) + .build()); + } + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java index 1d95a39afb6..d5be336bce6 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java @@ -66,6 +66,8 @@ public class DbVersion202503 implements DbVersion { .add(2025_03_027, "Drop change_type from SCA issues releases changes", DropChangeTypeFromScaIssuesReleasesChangesTable.class) .add(2025_03_028, "Remove duplicates from SCA releases table", MigrateRemoveDuplicateScaReleases.class) .add(2025_03_029, "Create unique index on SCA releases table", CreateUniqueIndexOnScaReleases.class) + .add(2025_03_030, "Create SCA analyses table", CreateScaAnalysesTable.class) + .add(2025_03_031, "Create unique index on SCA analyses table", CreateUniqueIndexOnScaAnalyses.class) ; } diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java index e898a27dbb7..2f624caebef 100644 --- a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java +++ b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java @@ -21,10 +21,14 @@ package org.sonar.server.component.index; import java.util.Arrays; import java.util.Collection; +import java.util.Optional; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTester; import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -33,15 +37,18 @@ import org.sonar.db.component.BranchDto; import org.sonar.db.component.ProjectData; import org.sonar.db.entity.EntityDto; import org.sonar.db.es.EsQueueDto; +import org.sonar.db.portfolio.PortfolioDto; import org.sonar.db.project.ProjectDto; import org.sonar.server.es.EsClient; import org.sonar.server.es.EsTester; import org.sonar.server.es.Indexers; import org.sonar.server.es.IndexingResult; +import static java.lang.String.format; import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.sonar.db.component.ComponentQualifiers.PROJECT; import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME; @@ -60,10 +67,18 @@ public class EntityDefinitionIndexerIT { public EsTester es = EsTester.create(); @Rule public DbTester db = DbTester.create(system2); + @Rule + public LogTester logTester = new LogTester(); private DbClient dbClient = db.getDbClient(); private DbSession dbSession = db.getSession(); - private EntityDefinitionIndexer underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client()); + private EntityDefinitionIndexer underTest; + + @Before + public void setup() { + underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client()); + logTester.setLevel(Level.DEBUG); + } @Test public void test_getIndexTypes() { @@ -121,6 +136,62 @@ public class EntityDefinitionIndexerIT { } @Test + public void indexOnStartup_fixes_corrupted_portfolios_if_possible_and_then_indexes_them() throws Exception { + underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client()); + String uuid = "portfolioUuid1"; + ProjectDto project = db.components().insertPrivateProject().getProjectDto(); + PortfolioDto corruptedPortfolio = new PortfolioDto() + .setKey("portfolio1") + .setName("My Portfolio") + .setSelectionMode(PortfolioDto.SelectionMode.NONE) + .setUuid(uuid) + .setRootUuid(uuid); + db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false); + + // corrupt the portfolio in a fixable way (root portfolio with self-referential parent_uuid) + dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = '%s' where uuid = '%s'", uuid, uuid)) + .execute(); + dbSession.commit(); + Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid); + + assertThat(entity).isPresent(); + assertThat(entity.get().getAuthUuid()).isNull(); + + underTest.indexOnStartup(emptySet()); + + assertThat(logTester.logs()).contains("Fixing corrupted portfolio tree for root portfolio " + corruptedPortfolio.getUuid()); + assertThatIndexContainsOnly(project, corruptedPortfolio); + } + + @Test + public void indexOnStartup_logs_warning_about_corrupted_portfolios_that_cannot_be_fixed_automatically() throws Exception { + underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client()); + String uuid = "portfolioUuid1"; + PortfolioDto corruptedPortfolio = new PortfolioDto() + .setKey("portfolio1") + .setName("My Portfolio") + .setSelectionMode(PortfolioDto.SelectionMode.NONE) + .setUuid(uuid) + .setRootUuid(uuid); + db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false); + + // corrupt the portfolio in an un-fixable way (non-existent parent) + dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = 'junk_uuid' where uuid = '%s'", uuid)) + .execute(); + dbSession.commit(); + Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid); + + assertThat(entity).isPresent(); + assertThat(entity.get().getAuthUuid()).isNull(); + + assertThatException() + .isThrownBy(() -> underTest.indexOnStartup(emptySet())); + + assertThat(logTester.logs()).contains("Detected portfolio tree corruption for portfolio " + corruptedPortfolio.getUuid()); + + } + + @Test public void indexOnAnalysis_indexes_project() { ProjectData project = db.components().insertPrivateProject(); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java index 9a9a4cb96f3..a2a2a55ae9c 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java @@ -30,6 +30,8 @@ import java.util.stream.Collectors; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; @@ -56,7 +58,7 @@ import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COM * Indexes the definition of all entities: projects, applications, portfolios and sub-portfolios. */ public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, NeedAuthorizationIndexer { - + private static final Logger LOG = LoggerFactory.getLogger(EntityDefinitionIndexer.class); private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_COMPONENT, entity -> true); private static final Set<IndexType> INDEX_TYPES = Set.of(TYPE_COMPONENT); @@ -172,16 +174,43 @@ public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, N private void doIndexByEntityUuid(Size bulkSize) { BulkIndexer bulk = new BulkIndexer(esClient, TYPE_COMPONENT, bulkSize); bulk.start(); + Set<EntityDto> corruptedEntities = new HashSet<>(); try (DbSession dbSession = dbClient.openSession(false)) { dbClient.entityDao().scrollForIndexing(dbSession, context -> { EntityDto dto = context.getResultObject(); - bulk.add(toDocument(dto).toIndexRequest()); + if (dto.getAuthUuid() == null) { + corruptedEntities.add(dto); + } else { + bulk.add(toDocument(dto).toIndexRequest()); + } }); + if (!corruptedEntities.isEmpty()) { + attemptToFixCorruptedEntities(dbSession, corruptedEntities); + List<EntityDto> fixedEntities = dbClient.entityDao().selectByUuids(dbSession, corruptedEntities.stream().map(EntityDto::getUuid).toList()); + fixedEntities.forEach(entity -> bulk.add(toDocument(entity).toIndexRequest())); + } } bulk.stop(); } + private void attemptToFixCorruptedEntities(DbSession dbSession, Set<EntityDto> corruptedEntities) { + for (EntityDto entity : corruptedEntities) { + dbClient.portfolioDao().selectByUuid(dbSession, entity.getUuid()).ifPresent(portfolio -> { + String portfolioUuid = portfolio.getUuid(); + String rootUuid = portfolio.getRootUuid(); + String parentUuid = portfolio.getParentUuid(); + if (portfolioUuid.equals(rootUuid) && portfolioUuid.equals(parentUuid)) { + LOG.warn("Fixing corrupted portfolio tree for root portfolio {}", portfolioUuid); + portfolio.setParentUuid(null); + dbClient.portfolioDao().update(dbSession, portfolio); + } else { + LOG.warn("Detected portfolio tree corruption for portfolio {}", portfolioUuid); + } + }); + } + } + private static void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) { SearchRequest searchRequest = EsClient.prepareSearch(TYPE_COMPONENT.getMainType()) .source(new SearchSourceBuilder().query(QueryBuilders.termQuery(ComponentIndexDefinition.FIELD_UUID, projectUuid))) diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java index 1d3e9eaed18..e3fc380c07f 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java @@ -79,7 +79,7 @@ public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEma format("<br/>If this token is still needed, please consider <a href=\"%s/account/security/\">generating</a> an equivalent.<br/><br/>", server.getPublicRootUrl())) .append("Don't forget to update the token in the locations where it is in use. " + "This may include the CI pipeline that analyzes your projects, " - + "the IDE settings that connect SonarLint to SonarQube, " + + "the IDE settings that connect SonarQube IDE to SonarQube Server, " + "and any places where you make calls to web services."); return builder.toString(); } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java index ba45ed43dc4..d0ba6ca7224 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java @@ -70,7 +70,7 @@ class TokenExpirationEmailComposerTest { + "Last used on: January 01, 2022<br/>" + "Expires on: %s<br/><br/>" + "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>" - + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.", + + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.", parseDate(expiredDate), parseDate(expiredDate))); } @@ -91,7 +91,7 @@ class TokenExpirationEmailComposerTest { + "Last used on: January 01, 2022<br/>" + "Expired on: %s<br/><br/>" + "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>" - + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.", + + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.", parseDate(expiredDate))); } diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index cb5cb05bc90..c814c91281f 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -207,6 +207,7 @@ import org.sonar.server.platform.telemetry.TelemetrySubportfolioSelectionModePro import org.sonar.server.platform.telemetry.TelemetryUserEnabledProvider; import org.sonar.server.platform.telemetry.TelemetryVersionProvider; import org.sonar.server.platform.web.ActionDeprecationLoggerInterceptor; +import org.sonar.server.platform.web.NoCacheFilter; import org.sonar.server.platform.web.SonarQubeIdeConnectionFilter; import org.sonar.server.platform.web.WebServiceFilter; import org.sonar.server.platform.web.WebServiceReroutingFilter; @@ -423,6 +424,7 @@ public class PlatformLevel4 extends PlatformLevel { new WebServicesWsModule(), SonarQubeIdeConnectionFilter.class, WebServiceFilter.class, + NoCacheFilter.class, WebServiceReroutingFilter.class, // localization diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java index d81006a91e3..4ef6d79166e 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java @@ -26,6 +26,7 @@ import org.sonar.server.platform.db.migration.AutoDbMigration; import org.sonar.server.platform.db.migration.DatabaseMigrationImpl; import org.sonar.server.platform.db.migration.MigrationEngineModule; import org.sonar.server.platform.db.migration.NoopDatabaseMigrationImpl; +import org.sonar.server.platform.web.NoCacheFilter; import org.sonar.server.platform.web.WebServiceFilter; import org.sonar.server.platform.ws.IndexAction; import org.sonar.server.platform.ws.L10nWs; @@ -59,6 +60,7 @@ public class PlatformLevelSafeMode extends PlatformLevel { SafeModeUserSession.class, WebServiceEngine.class, WebServiceFilter.class, + NoCacheFilter.class, // Monitoring ServerMonitoringMetrics.class); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java new file mode 100644 index 00000000000..268bafd3aed --- /dev/null +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.web; + +import org.sonar.api.server.http.HttpRequest; +import org.sonar.api.server.http.HttpResponse; +import org.sonar.api.web.FilterChain; +import org.sonar.api.web.HttpFilter; +import java.io.IOException; +import org.sonar.api.web.UrlPattern; + +public class NoCacheFilter extends HttpFilter { + + @Override + public void doFilter(HttpRequest httpRequest, HttpResponse httpResponse, FilterChain filterChain) throws IOException { + httpResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + filterChain.doFilter(httpRequest, httpResponse); + } + + /** + * The Cache-Control for API v1 is handled in the org.sonar.server.ws.ServletResponse + */ + @Override + public UrlPattern doGetPattern() { + return UrlPattern.builder() + .includes("/api/v2/*") + .build(); + } +} diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java new file mode 100644 index 00000000000..18b54291215 --- /dev/null +++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.web; + +import org.sonar.api.server.http.HttpRequest; +import org.sonar.api.server.http.HttpResponse; +import org.sonar.api.web.FilterChain; +import org.junit.Test; +import org.sonar.api.web.UrlPattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class NoCacheFilterTest { + + private final NoCacheFilter filter = new NoCacheFilter(); + + @Test + public void doGetPattern_whenAPIv2_patternMatches() { + UrlPattern urlPattern = filter.doGetPattern(); + + assertThat(urlPattern.matches("/api/v2/whatever")).isTrue(); + } + + @Test + public void doGetPattern_whenAPIv1_patternDoesNotMatch() { + UrlPattern urlPattern = filter.doGetPattern(); + + assertThat(urlPattern.matches("/api/whatever")).isFalse(); + } + + @Test + public void doFilter_setResponseHeader() throws Exception{ + HttpResponse response = mock(); + HttpRequest request = mock(); + FilterChain chain = mock(); + + filter.doFilter(request, response, chain); + verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + } +} |