diff options
9 files changed, 136 insertions, 6 deletions
diff --git a/build.gradle b/build.gradle index a57fc2c5e5e..453320692d3 100644 --- a/build.gradle +++ b/build.gradle @@ -324,8 +324,8 @@ subprojects { dependency "org.sonarsource.api.plugin:sonar-plugin-api:$pluginApiVersion" dependency "org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures:$pluginApiVersion" dependency 'org.sonarsource.xml:sonar-xml-plugin:2.13.0.5938' - dependency 'org.sonarsource.iac:sonar-iac-plugin:1.45.0.14930' - dependency 'com.sonarsource.iac:sonar-iac-enterprise-plugin:1.45.0.14930' + dependency 'org.sonarsource.iac:sonar-iac-plugin:1.46.0.15097' + dependency 'com.sonarsource.iac:sonar-iac-enterprise-plugin:1.46.0.15097' dependency 'org.sonarsource.text:sonar-text-plugin:2.21.1.5779' dependency 'com.sonarsource.text:sonar-text-developer-plugin:2.21.1.5779' dependency 'com.sonarsource.text:sonar-text-enterprise-plugin:2.21.1.5779' diff --git a/gradle.properties b/gradle.properties index ab5ca3c6f6c..6580826a45e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,4 @@ elasticSearchServerVersion=8.16.3 projectType=application artifactoryUrl=https://repox.jfrog.io/repox jre_release_name=jdk-17.0.13+11 -webappVersion=2025.3.0.15992 +webappVersion=2025.3.0.16221 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..10dda0d3735 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 @@ -2018,6 +2018,23 @@ oldCreationDate)); assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).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) { return new AnticipatedTransitionDto(uuid, projectUuid, "userUuid", "transition", null, null, null, null, "rule:key", "filepath", creationDate.getTime()); 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..e0fb94fa09a 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); 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..79ebb206068 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) { 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..085b28cef6c 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 @@ -200,4 +200,6 @@ public interface PurgeMapper { void deleteScaIssuesReleasesByComponentUuid(@Param("componentUuid") String componentUuid); void deleteScaReleasesByComponentUuid(@Param("componentUuid") String componentUuid); + + void deleteArchitectureGraphsByBranchUuid(@Param("branchUuid") String branchUuid); } 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..f8d213b6a3b 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,6 +670,9 @@ 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="deleteScaDependenciesByComponentUuid"> delete from sca_dependencies where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR}) 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))) |