]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11021 Add a migration for SonarCloud
authorEric Hartmann <hartmann.eric@gmail.com>
Fri, 13 Jul 2018 12:21:59 +0000 (14:21 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 17 Jul 2018 18:21:27 +0000 (20:21 +0200)
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizations.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/FixMissingQualityProfilesOnOrganizationsTest/schema.sql [new file with mode: 0644]

index 2f0fbdf88caaa586c54fe2bef202cf0341d42964..1d17020b7ef9a0ea3450bab669f9da9b9a89c7a7 100644 (file)
@@ -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 (file)
index 0000000..32f796d
--- /dev/null
@@ -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<BuiltInQProfile> reduceBuiltInQualityProfiles(Context context) throws SQLException {
+    ListMultimap<String, BuiltInQProfile> builtInQPByLanguages = ArrayListMultimap.create();
+
+    List<BuiltInQProfile> 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<BuiltInQProfile> 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;
+    }
+  }
+}
index 39e960a75fd84f1bf65601b3b47a31ecedae63a9..2bb8434ebef81e3493fa5c69b21ca80ebd2d9a00 100644 (file)
@@ -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 (file)
index 0000000..91dfe3a
--- /dev/null
@@ -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 (file)
index 0000000..7422284
--- /dev/null
@@ -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");