From: Eric Hartmann Date: Fri, 13 Jul 2018 12:21:59 +0000 (+0200) Subject: SONAR-11021 Add a migration for SonarCloud X-Git-Tag: 7.5~780 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8bbe57990ec82e2419925b13b90a1e0869d09512;p=sonarqube.git SONAR-11021 Add a migration for SonarCloud --- diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java index 2f0fbdf88ca..1d17020b7ef 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java @@ -36,6 +36,7 @@ public class DbVersion73 implements DbVersion { .add(2206, "Add SUBSCRIPTION column to ORGANIZATIONS table", AddSubscriptionToOrganizations.class) .add(2207, "Populate SUBSCRIPTION in ORGANIZATIONS", PopulateSubscriptionOnOrganizations.class) .add(2208, "Add rules.security_standards", AddSecurityStandardsToRules.class) + .add(2209, "Fix missing quality profiles on organizations", FixMissingQualityProfilesOnOrganizations.class) ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizations.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizations.java new file mode 100644 index 00000000000..32f796d363d --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizations.java @@ -0,0 +1,166 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.v73; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import java.sql.SQLException; +import java.util.List; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.SupportsBlueGreen; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; + +import static java.util.stream.Collectors.joining; + +@SupportsBlueGreen +// SONAR-11021 +public class FixMissingQualityProfilesOnOrganizations extends DataChange { + + private final System2 system2; + private final UuidFactory uuidFactory; + private final Configuration configuration; + + public FixMissingQualityProfilesOnOrganizations(Database db, System2 system2, UuidFactory uuidFactory, Configuration configuration) { + super(db); + this.system2 = system2; + this.uuidFactory = uuidFactory; + this.configuration = configuration; + } + + @Override + protected void execute(Context context) throws SQLException { + if (!configuration.getBoolean("sonar.sonarcloud.enabled").orElse(false)) { + return; + } + + long now = system2.now(); + + insertMissingOrgQProfiles(context, now); + insertMissingDefaultQProfiles(context, now); + } + + private void insertMissingOrgQProfiles(Context context, long now) throws SQLException { + MassUpdate massUpdate = context + .prepareMassUpdate() + .rowPluralName("Organization quality profiles"); + massUpdate.select("SELECT o.uuid, rp.kee FROM organizations o, rules_profiles rp " + + "WHERE rp.is_built_in = ?" + + "AND NOT EXISTS(SELECT(1) FROM org_qprofiles oqp WHERE oqp.organization_uuid = o.uuid AND oqp.rules_profile_uuid = rp.kee)") + .setBoolean(1, true); + massUpdate.update("INSERT INTO org_qprofiles (uuid, organization_uuid, rules_profile_uuid, created_at, updated_at) VALUES(?, ?, ?, ?, ?)"); + massUpdate.execute((row, update) -> { + String organizationUuid = row.getString(1); + String rulesProfileUuid = row.getString(2); + update.setString(1, uuidFactory.create()); + update.setString(2, organizationUuid); + update.setString(3, rulesProfileUuid); + update.setLong(4, now); + update.setLong(5, now); + + return true; + }); + } + + private static void insertMissingDefaultQProfiles(Context context, long now) throws SQLException { + String defaultRulesProfileKees = reduceBuiltInQualityProfiles(context) + .stream() + .map(qp -> "'" + qp.kee + "'") + .collect(joining(",")); + + if (defaultRulesProfileKees.isEmpty()) { + return; + } + + MassUpdate massUpdate = context + .prepareMassUpdate() + .rowPluralName("Organization default quality profiles"); + massUpdate.select("SELECT o.uuid, oqp.uuid, rp.language FROM organizations o, org_qprofiles oqp, rules_profiles rp " + + "WHERE oqp.rules_profile_uuid = rp.kee " + + "AND oqp.organization_uuid = o.uuid " + + "AND rp.kee IN ( " + defaultRulesProfileKees + " ) " + + "AND NOT EXISTS(SELECT(1) FROM default_qprofiles dqp WHERE dqp.organization_uuid = o.uuid AND dqp.language = rp.language)"); + massUpdate.update("INSERT INTO default_qprofiles (organization_uuid, language, qprofile_uuid, created_at, updated_at) VALUES(?, ?, ?, ?, ?)"); + massUpdate.execute((row, update) -> { + String organizationUuid = row.getString(1); + String orgQProfileUuid = row.getString(2); + String language = row.getString(3); + update.setString(1, organizationUuid); + update.setString(2, language); + update.setString(3, orgQProfileUuid); + update.setLong(4, now); + update.setLong(5, now); + + return true; + }); + } + + /** + * Return the list of preferred built-in quality profiles. + * In the current state of database, the information of the "by-default" one is absent (handled by plugin). + * Let's choose the "Sonar Way" one, fallbacking to the first one + * + * This methods is returning the list of rules_profiles kee of built-in quality profiles with one by language + */ + private static List reduceBuiltInQualityProfiles(Context context) throws SQLException { + ListMultimap builtInQPByLanguages = ArrayListMultimap.create(); + + List builtInQProfiles = context.prepareSelect("SELECT kee, language, name FROM rules_profiles WHERE is_built_in = ?") + .setBoolean(1, true) + .list(row -> new BuiltInQProfile(row.getString(1), row.getString(2), row.getString(3))); + + builtInQProfiles.forEach(builtInQProfile -> builtInQPByLanguages.put(builtInQProfile.language, builtInQProfile)); + + // Filter all built in quality profiles to have only one by language + // And prefer the one named "Sonar Way" + builtInQPByLanguages.keySet().forEach(l -> { + List qps = builtInQPByLanguages.get(l); + if (qps.size() > 1) { + BuiltInQProfile sonarWay = qps.stream().filter(qp -> qp.name.equals("Sonar way")) + .findFirst() + .orElse(qps.get(0)); + qps.forEach(qp -> { + if (qp.kee.equals(sonarWay.kee)) { + return; + } + builtInQProfiles.remove(qp); + }); + } + }); + + return builtInQProfiles; + } + + private static class BuiltInQProfile { + private final String kee; + private final String language; + private final String name; + + public BuiltInQProfile(String kee, String language, String name) { + this.kee = kee; + this.language = language; + this.name = name; + } + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java index 39e960a75fd..2bb8434ebef 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java @@ -21,6 +21,7 @@ package org.sonar.server.platform.db.migration.version.v73; import org.junit.Test; +import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationCount; import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber; public class DbVersion73Test { @@ -32,4 +33,8 @@ public class DbVersion73Test { verifyMinimumMigrationNumber(underTest, 2200); } + @Test + public void verify_migration_count() { + verifyMigrationCount(underTest, 10); + } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest.java new file mode 100644 index 00000000000..91dfe3a762e --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest.java @@ -0,0 +1,203 @@ +package org.sonar.server.platform.db.migration.version.v73;/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ + +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.stream.Collectors; +import org.assertj.core.groups.Tuple; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.core.util.Uuids; +import org.sonar.db.CoreDbTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class FixMissingQualityProfilesOnOrganizationsTest { + private final static long PAST = 10_000_000_000L; + private final static long NOW = 50_000_000_000L; + + @Rule + public CoreDbTester db = CoreDbTester.createForSchema(FixMissingQualityProfilesOnOrganizationsTest.class, "schema.sql"); + + private MapSettings settings = new MapSettings(); + private System2 system2 = new TestSystem2().setNow(NOW); + private FixMissingQualityProfilesOnOrganizations underTest = new FixMissingQualityProfilesOnOrganizations(db.database(), system2, + UuidFactoryFast.getInstance(), settings.asConfig()); + + @Test + public void migration_is_reentrant_on_sonarqube() throws SQLException { + underTest.execute(); + underTest.execute(); + } + + @Test + public void create_missing_links_with_builtin() throws SQLException { + setSonarCloud(); + String orgUuid = insertOrganization(); + String qProfileUuid = insertRulesProfiles("xoo profile", "xoo", true); + + underTest.execute(); + + assertDefaultQProfiles(tuple(orgUuid, "xoo", retrieveOrgQProfile(orgUuid, qProfileUuid))); + assertOrgQProfiles(tuple(orgUuid, qProfileUuid)); + } + + @Test + public void does_nothing_when_no_missing_built_in_profile() throws SQLException { + setSonarCloud(); + insertOrganization(); + insertRulesProfiles("xoo profile", "xoo", false); + + underTest.execute(); + + assertDefaultQProfiles(); + assertOrgQProfiles(); + } + + @Test + public void prefer_SonarWay_BuiltIn_quality_profile_as_default() throws SQLException { + setSonarCloud(); + String orgUuid = insertOrganization(); + String anotherRuleProfileUuid = insertRulesProfiles("xoo profile", "xoo", true); + String sonarWayQProfileUuid = insertRulesProfiles("Sonar way", "xoo", true); + + underTest.execute(); + assertOrgQProfiles( + tuple(orgUuid, anotherRuleProfileUuid), + tuple(orgUuid, sonarWayQProfileUuid) + ); + assertDefaultQProfiles(tuple(orgUuid, "xoo", retrieveOrgQProfile(orgUuid, sonarWayQProfileUuid))); + } + + @Test + public void dont_create_duplicates_when_records_exist() throws SQLException { + setSonarCloud(); + String orgUuid = insertOrganization(); + String qProfileUuid = insertRulesProfiles("xoo profile","xoo", true); + insertDefaultQProfiles(orgUuid, "xoo", qProfileUuid); + insertOrgQProfiles(orgUuid, qProfileUuid); + + underTest.execute(); + + assertDefaultQProfiles(tuple(orgUuid, "xoo", qProfileUuid)); + assertOrgQProfiles(tuple(orgUuid, qProfileUuid)); + } + + @Test + public void create_missing_default_qprofiles() throws SQLException { + setSonarCloud(); + String orgUuid = insertOrganization(); + String qProfileUuid = insertRulesProfiles("xoo profile","xoo", true); + String orgQProfileUuid = insertOrgQProfiles(orgUuid, qProfileUuid); + + underTest.execute(); + + assertDefaultQProfiles(tuple(orgUuid, "xoo", orgQProfileUuid)); + assertOrgQProfiles(tuple(orgUuid, qProfileUuid)); + } + + @Test + public void migration_is_reentrant_on_sonarcloud() throws SQLException { + setSonarCloud(); + + underTest.execute(); + underTest.execute(); + } + + private String insertOrganization() { + String uuid = Uuids.createFast(); + db.executeInsert("ORGANIZATIONS", + "UUID", uuid, + "KEE", uuid, + "NAME", uuid, + "SUBSCRIPTION", "PAID", + "GUARDED", false, + "DEFAULT_QUALITY_GATE_UUID", "QGATE_UUID", + "NEW_PROJECT_PRIVATE", false, + "CREATED_AT", PAST, + "UPDATED_AT", PAST); + return uuid; + } + + private String insertRulesProfiles(String name, String language, Boolean isBuiltIn) { + String uuid = Uuids.createFast(); + db.executeInsert("RULES_PROFILES", + "NAME", name, + "LANGUAGE", language, + "KEE", uuid, + "CREATED_AT", new Timestamp(PAST), + "UPDATED_AT", new Timestamp(PAST), + "IS_BUILT_IN", isBuiltIn); + return uuid; + } + + private String insertOrgQProfiles(String organizationUuid, String rulesProfileUuid) { + String uuid = Uuids.createFast(); + db.executeInsert("ORG_QPROFILES", + "UUID", uuid, + "ORGANIZATION_UUID", organizationUuid, + "RULES_PROFILE_UUID", rulesProfileUuid, + "CREATED_AT", PAST, + "UPDATED_AT", PAST); + return uuid; + } + + private void insertDefaultQProfiles(String organizationUuid, String language, String qProfileUuid) { + db.executeInsert("DEFAULT_QPROFILES", + "ORGANIZATION_UUID", organizationUuid, + "LANGUAGE", language, + "QPROFILE_UUID", qProfileUuid, + "CREATED_AT", PAST, + "UPDATED_AT", PAST); + } + + private String retrieveOrgQProfile(String organizationUuid, String qProfileUuid) { + return (String) db.select("SELECT UUID FROM ORG_QPROFILES " + + "WHERE ORGANIZATION_UUID='" + organizationUuid + "' AND RULES_PROFILE_UUID='" + qProfileUuid + "'") + .iterator() + .next() + .get("UUID"); + } + + private void assertOrgQProfiles(Tuple... expectedTuples) { + assertThat(db.select("SELECT ORGANIZATION_UUID, RULES_PROFILE_UUID FROM ORG_QPROFILES") + .stream() + .map(row -> new Tuple(row.get("ORGANIZATION_UUID"), row.get("RULES_PROFILE_UUID"))) + .collect(Collectors.toList())) + .containsExactlyInAnyOrder(expectedTuples); + } + + private void assertDefaultQProfiles(Tuple... expectedTuples) { + assertThat(db.select("SELECT ORGANIZATION_UUID, LANGUAGE, QPROFILE_UUID FROM DEFAULT_QPROFILES") + .stream() + .map(row -> new Tuple(row.get("ORGANIZATION_UUID"), row.get("LANGUAGE"), row.get("QPROFILE_UUID"))) + .collect(Collectors.toList())) + .containsExactlyInAnyOrder(expectedTuples); + } + + private void setSonarCloud() { + settings.setProperty("sonar.sonarcloud.enabled", true); + } +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest/schema.sql new file mode 100644 index 00000000000..74222845568 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest/schema.sql @@ -0,0 +1,59 @@ + +CREATE TABLE "ORG_QPROFILES" ( + "UUID" VARCHAR(255) NOT NULL, + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "RULES_PROFILE_UUID" VARCHAR(255) NOT NULL, + "PARENT_UUID" VARCHAR(255), + "LAST_USED" BIGINT, + "USER_UPDATED_AT" BIGINT, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL, + + CONSTRAINT "PK_ORG_QPROFILES" PRIMARY KEY ("UUID") +); +CREATE INDEX "ORG_QPROFILES_ORG_UUID" ON "ORG_QPROFILES" ("ORGANIZATION_UUID"); +CREATE INDEX "ORG_QPROFILES_RP_UUID" ON "ORG_QPROFILES" ("RULES_PROFILE_UUID"); + +CREATE TABLE "DEFAULT_QPROFILES" ( + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "LANGUAGE" VARCHAR(20) NOT NULL, + "QPROFILE_UUID" VARCHAR(255) NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL, + + CONSTRAINT "PK_DEFAULT_QPROFILES" PRIMARY KEY ("ORGANIZATION_UUID", "LANGUAGE") +); +CREATE UNIQUE INDEX "UNIQ_DEFAULT_QPROFILES_UUID" ON "DEFAULT_QPROFILES" ("QPROFILE_UUID"); + +CREATE TABLE "RULES_PROFILES" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "NAME" VARCHAR(100) NOT NULL, + "LANGUAGE" VARCHAR(20), + "KEE" VARCHAR(255) NOT NULL, + "RULES_UPDATED_AT" VARCHAR(100), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP, + "IS_BUILT_IN" BOOLEAN NOT NULL +); +CREATE UNIQUE INDEX "UNIQ_QPROF_KEY" ON "RULES_PROFILES" ("KEE"); + +CREATE TABLE "ORGANIZATIONS" ( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(32) NOT NULL, + "NAME" VARCHAR(64) NOT NULL, + "DESCRIPTION" VARCHAR(256), + "URL" VARCHAR(256), + "AVATAR_URL" VARCHAR(256), + "GUARDED" BOOLEAN NOT NULL, + "DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40), + "DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40), + "DEFAULT_GROUP_ID" INTEGER, + "DEFAULT_QUALITY_GATE_UUID" VARCHAR(40) NOT NULL, + "NEW_PROJECT_PRIVATE" BOOLEAN NOT NULL, + "SUBSCRIPTION" VARCHAR(40), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL, + + CONSTRAINT "PK_ORGANIZATIONS" PRIMARY KEY ("UUID") +); +CREATE UNIQUE INDEX "ORGANIZATION_KEY" ON "ORGANIZATIONS" ("KEE");