diff options
author | Janos Gyerik <janos.gyerik@sonarsource.com> | 2019-07-03 11:11:55 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-07-05 20:21:13 +0200 |
commit | 478a71d5d0b21479c034836bedfc88266873359f (patch) | |
tree | ffee62feee4a8bb7f38bd8d4c1a97f69122269e3 | |
parent | cdafda526fa26697b976a14187b7f210a257a75e (diff) | |
download | sonarqube-478a71d5d0b21479c034836bedfc88266873359f.tar.gz sonarqube-478a71d5d0b21479c034836bedfc88266873359f.zip |
SC-799 Add temporary live migration to delete empty personal orgs
8 files changed, 304 insertions, 1 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java index ceac98a58df..da63b59d512 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java @@ -35,6 +35,7 @@ public class OrganizationQuery { private final boolean onlyTeam; private final boolean onlyPersonal; private final boolean withAnalyses; + private final boolean withoutProjects; @Nullable private final Long analyzedAfter; @@ -48,6 +49,10 @@ public class OrganizationQuery { } this.withAnalyses = builder.withAnalyses; this.analyzedAfter = builder.analyzedAfter; + this.withoutProjects = builder.withoutProjects; + if ((this.withAnalyses || this.analyzedAfter != null) && this.withoutProjects) { + throw new IllegalArgumentException("withoutProjects cannot be used together with withAnalyses or analyzedAfter"); + } } @CheckForNull @@ -77,6 +82,10 @@ public class OrganizationQuery { return analyzedAfter; } + public boolean isWithoutProjects() { + return withoutProjects; + } + public static OrganizationQuery returnAll() { return NO_FILTER; } @@ -92,6 +101,7 @@ public class OrganizationQuery { private boolean onlyTeam = false; private boolean onlyPersonal = false; private boolean withAnalyses = false; + private boolean withoutProjects = false; @Nullable private Long analyzedAfter; @@ -133,6 +143,11 @@ public class OrganizationQuery { return this; } + public Builder setWithoutProjects() { + this.withoutProjects = true; + return this; + } + public OrganizationQuery build() { return new OrganizationQuery(this); } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml index 563c671fe04..a806a01d78a 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml @@ -147,6 +147,14 @@ and s.islast = ${_true} ) </if> + <if test="query.withoutProjects"> + and not exists( + select 1 + from projects p + where p.organization_uuid = org.uuid + and p.enabled = ${_true} + ) + </if> <if test="query.analyzedAfter != null"> and exists( select 1 diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java index 02df65d8f24..ef389bc5223 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java @@ -630,6 +630,25 @@ public class OrganizationDaoTest { } @Test + public void selectByQuery_filter_on_withoutProjects() { + assertThat(selectUuidsByQuery(q -> q.setWithoutProjects(), forPage(1).andSize(100))) + .isEmpty(); + + // has projects + OrganizationDto orgWithProjects = db.organizations().insert(); + db.components().insertPrivateProject(orgWithProjects); + db.components().insertPrivateProject(orgWithProjects, p -> p.setEnabled(false)); + // has no projects + OrganizationDto orgWithoutProjects = db.organizations().insert(); + // has only disabled projects + OrganizationDto orgWithOnlyDisabledProjects = db.organizations().insert(); + db.components().insertPrivateProject(orgWithOnlyDisabledProjects, p -> p.setEnabled(false)); + + assertThat(selectUuidsByQuery(q -> q.setWithoutProjects(), forPage(1).andSize(100))) + .containsExactlyInAnyOrder(orgWithoutProjects.getUuid(), orgWithOnlyDisabledProjects.getUuid()); + } + + @Test public void getDefaultTemplates_returns_empty_when_table_is_empty() { assertThat(underTest.getDefaultTemplates(dbSession, ORGANIZATION_DTO_1.getUuid())).isEmpty(); } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java new file mode 100644 index 00000000000..74f60141463 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.db.organization; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class OrganizationQueryTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void throws_IAE_when_both_onlyPersonal_and_onlyTeam_are_set() { + expectedException.expect(IllegalArgumentException.class); + OrganizationQuery.newOrganizationQueryBuilder() + .setOnlyPersonal() + .setOnlyTeam() + .build(); + } + + @Test + public void throws_IAE_when_withoutProjects_is_used_together_with_withAnalyses() { + expectedException.expect(IllegalArgumentException.class); + OrganizationQuery.newOrganizationQueryBuilder() + .setWithoutProjects() + .setWithAnalyses() + .build(); + } + + @Test + public void throws_IAE_when_withoutProjects_is_used_together_with_analyzedAfter() { + expectedException.expect(IllegalArgumentException.class); + OrganizationQuery.newOrganizationQueryBuilder() + .setWithoutProjects() + .setAnalyzedAfter(1) + .build(); + } + + @Test + public void throws_IAE_when_withoutProjects_is_used_together_with_both_withAnalyses_and_analyzedAfter() { + expectedException.expect(IllegalArgumentException.class); + OrganizationQuery.newOrganizationQueryBuilder() + .setWithoutProjects() + .setWithAnalyses() + .setAnalyzedAfter(1) + .build(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java new file mode 100644 index 00000000000..1dc06bd8f3a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.organization.ws; + +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.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.organization.OrganizationQuery; +import org.sonar.server.user.UserSession; + +public class DeleteEmptyPersonalOrgsAction implements OrganizationsWsAction { + + private static final Logger LOGGER = Loggers.get(DeleteEmptyPersonalOrgsAction.class); + + private static final String ACTION = "delete_empty_personal_orgs"; + + private final UserSession userSession; + private final OrganizationDeleter organizationDeleter; + + public DeleteEmptyPersonalOrgsAction(UserSession userSession, OrganizationDeleter organizationDeleter) { + this.userSession = userSession; + this.organizationDeleter = organizationDeleter; + } + + @Override + public void define(WebService.NewController context) { + context.createAction(ACTION) + .setDescription("Internal use. Requires system administration permission. Delete empty personal organizations.") + .setInternal(true) + .setPost(true) + .setHandler(this); + } + + @Override + public void handle(Request request, Response response) throws Exception { + userSession.checkLoggedIn().checkIsSystemAdministrator(); + + LOGGER.info("deleting empty personal organizations"); + + OrganizationQuery query = OrganizationQuery.newOrganizationQueryBuilder() + .setOnlyPersonal() + .setWithoutProjects() + .build(); + + organizationDeleter.deleteByQuery(query); + + response.noContent(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java index 4d9f5083e65..a19b95ebff3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java @@ -52,6 +52,7 @@ public class OrganizationsWsModule extends Module { CreateAction.class, OrganizationDeleter.class, DeleteAction.class, + DeleteEmptyPersonalOrgsAction.class, RemoveMemberAction.class, UpdateAction.class); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java new file mode 100644 index 00000000000..3ccd6c99316 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.organization.ws; + +import java.util.Arrays; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ResourceTypesRule; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.component.ComponentCleanerService; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.ProjectIndexersImpl; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.organization.BillingValidationsProxyImpl; +import org.sonar.server.project.ProjectLifeCycleListener; +import org.sonar.server.project.ProjectLifeCycleListenersImpl; +import org.sonar.server.qualityprofile.QProfileFactoryImpl; +import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.user.index.UserIndexer; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.permission.OrganizationPermission.ADMINISTER; + +public class DeleteEmptyPersonalOrgsActionTest { + + @Rule + public final UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public final DbTester db = DbTester.create(new System2()); + private final DbClient dbClient = db.getDbClient(); + + @Rule + public final EsTester es = EsTester.create(); + private final EsClient esClient = es.client(); + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + + private final OrganizationDeleter organizationDeleter = new OrganizationDeleter(dbClient, + new ComponentCleanerService(dbClient, new ResourceTypesRule(), new ProjectIndexersImpl()), + new UserIndexer(dbClient, esClient), + new QProfileFactoryImpl(dbClient, UuidFactoryFast.getInstance(), new System2(), new ActiveRuleIndexer(dbClient, esClient)), + new ProjectLifeCycleListenersImpl(new ProjectLifeCycleListener[0]), + new BillingValidationsProxyImpl()); + + private final DeleteEmptyPersonalOrgsAction underTest = new DeleteEmptyPersonalOrgsAction(userSession, organizationDeleter); + private final WsActionTester ws = new WsActionTester(underTest); + + @Test + public void request_fails_if_user_is_not_root() { + userSession.logIn(); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + ws.newRequest().execute(); + } + + @Test + public void delete_empty_personal_orgs() { + OrganizationDto emptyPersonal = db.organizations().insert(o -> o.setGuarded(true)); + db.users().insertUser(u -> u.setOrganizationUuid(emptyPersonal.getUuid())); + + OrganizationDto nonEmptyPersonal = db.organizations().insert(o -> o.setGuarded(true)); + db.users().insertUser(u -> u.setOrganizationUuid(nonEmptyPersonal.getUuid())); + db.components().insertPublicProject(nonEmptyPersonal); + + OrganizationDto emptyRegular = db.organizations().insert(); + + OrganizationDto nonEmptyRegular = db.organizations().insert(); + db.components().insertPublicProject(nonEmptyRegular); + + UserDto admin = db.users().insertUser(); + db.users().insertPermissionOnUser(admin, ADMINISTER); + userSession.logIn().setSystemAdministrator(); + ws.newRequest().execute(); + + List<String> notDeleted = Arrays.asList( + db.getDefaultOrganization().getUuid(), + nonEmptyPersonal.getUuid(), + emptyRegular.getUuid(), + nonEmptyRegular.getUuid()); + + assertThat(dbClient.organizationDao().selectAllUuids(db.getSession())) + .containsExactlyInAnyOrderElementsOf(notDeleted); + } + + @Test + public void definition() { + WebService.Action action = ws.getDef(); + assertThat(action.isPost()).isTrue(); + assertThat(action.isInternal()).isTrue(); + assertThat(action.handler()).isNotNull(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/OrganizationsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/OrganizationsWsModuleTest.java index d6023ac7374..ce3eaed9160 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/OrganizationsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/OrganizationsWsModuleTest.java @@ -49,7 +49,7 @@ public class OrganizationsWsModuleTest { underTest.configure(container); assertThat(container.getPicoContainer().getComponentAdapters()) - .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 13); + .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 14); } } |