aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJanos Gyerik <janos.gyerik@sonarsource.com>2019-07-03 11:11:55 +0200
committerSonarTech <sonartech@sonarsource.com>2019-07-05 20:21:13 +0200
commit478a71d5d0b21479c034836bedfc88266873359f (patch)
treeffee62feee4a8bb7f38bd8d4c1a97f69122269e3
parentcdafda526fa26697b976a14187b7f210a257a75e (diff)
downloadsonarqube-478a71d5d0b21479c034836bedfc88266873359f.tar.gz
sonarqube-478a71d5d0b21479c034836bedfc88266873359f.zip
SC-799 Add temporary live migration to delete empty personal orgs
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java15
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml8
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java19
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java67
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java69
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java1
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java124
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/organization/ws/OrganizationsWsModuleTest.java2
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);
}
}