@@ -41,11 +41,15 @@ public class WebhookDao implements Dao { | |||
return Optional.ofNullable(mapper(dbSession).selectByUuid(uuid)); | |||
} | |||
public List<WebhookDto> selectByOrganizationUuid(DbSession dbSession, OrganizationDto organizationDto) { | |||
public List<WebhookDto> selectByOrganization(DbSession dbSession, OrganizationDto organizationDto) { | |||
return mapper(dbSession).selectForOrganizationUuidOrderedByName(organizationDto.getUuid()); | |||
} | |||
public List<WebhookDto> selectByProjectUuid(DbSession dbSession, ComponentDto componentDto) { | |||
public List<WebhookDto> selectByOrganizationUuid(DbSession dbSession, String organizationUuid) { | |||
return mapper(dbSession).selectForOrganizationUuidOrderedByName(organizationUuid); | |||
} | |||
public List<WebhookDto> selectByProject(DbSession dbSession, ComponentDto componentDto) { | |||
return mapper(dbSession).selectForProjectUuidOrderedByName(componentDto.uuid()); | |||
} | |||
@@ -41,6 +41,7 @@ public class DbVersion71 implements DbVersion { | |||
.add(2011, "Drop table PROJECT_LINKS", DropTableProjectLinks.class) | |||
.add(2012, "Rename table PROJECT_LINKS2 to PROJECT_LINKS", RenameTableProjectLinks2ToProjectLinks.class) | |||
.add(2013, "Create WEBHOOKS Table", CreateWebhooksTable.class) | |||
.add(2014, "Migrate webhooks from SETTINGS table to WEBHOOKS table", MigrateWebhooksToWebhooksTable.class) | |||
; | |||
} | |||
} |
@@ -0,0 +1,241 @@ | |||
/* | |||
* 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.v71; | |||
import java.sql.SQLException; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Function; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.Database; | |||
import org.sonar.server.platform.db.migration.step.DataChange; | |||
import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidProvider; | |||
import static java.util.stream.Collectors.toList; | |||
import static java.util.stream.Collectors.toMap; | |||
public class MigrateWebhooksToWebhooksTable extends DataChange { | |||
private static final Logger LOGGER = Loggers.get(MigrateWebhooksToWebhooksTable.class); | |||
private DefaultOrganizationUuidProvider defaultOrganizationUuidProvider; | |||
private UuidFactory uuidFactory; | |||
public MigrateWebhooksToWebhooksTable(Database db, DefaultOrganizationUuidProvider defaultOrganizationUuidProvider, UuidFactory uuidFactory) { | |||
super(db); | |||
this.defaultOrganizationUuidProvider = defaultOrganizationUuidProvider; | |||
this.uuidFactory = uuidFactory; | |||
} | |||
@Override | |||
public void execute(Context context) throws SQLException { | |||
Map<String, PropertyRow> rows = context | |||
.prepareSelect("select id, prop_key, resource_id, text_value, created_at from properties where prop_key like 'sonar.webhooks%'") | |||
.list(row -> new PropertyRow( | |||
row.getLong(1), | |||
row.getString(2), | |||
row.getLong(3), | |||
row.getString(4), | |||
row.getLong(5))) | |||
.stream() | |||
.collect(toMap(PropertyRow::key, Function.identity())); | |||
if (!rows.isEmpty()) { | |||
migrateGlobalWebhooks(context, rows); | |||
migrateProjectsWebhooks(context, rows); | |||
context | |||
.prepareUpsert("delete from properties where prop_key like 'sonar.webhooks.global%' or prop_key like 'sonar.webhooks.project%'") | |||
.execute() | |||
.commit(); | |||
} | |||
} | |||
private void migrateProjectsWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException { | |||
PropertyRow index = properties.get("sonar.webhooks.project"); | |||
if (index != null) { | |||
// can't lambda due to checked exception. | |||
for (Webhook webhook : extractProjectWebhooksFrom(context, properties, index.value().split(","))) { | |||
insert(context, webhook); | |||
} | |||
} | |||
} | |||
private void migrateGlobalWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException { | |||
PropertyRow index = properties.get("sonar.webhooks.global"); | |||
if (index != null) { | |||
// can't lambda due to checked exception. | |||
for (Webhook webhook : extractGlobalWebhooksFrom(context, properties, index.value().split(","))) { | |||
insert(context, webhook); | |||
} | |||
} | |||
} | |||
private void insert(Context context, Webhook webhook) throws SQLException { | |||
if (webhook.isValid()) { | |||
context.prepareUpsert("insert into webhooks (uuid, name, url, organization_uuid, project_uuid, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)") | |||
.setString(1, uuidFactory.create()) | |||
.setString(2, webhook.name()) | |||
.setString(3, webhook.url()) | |||
.setString(4, webhook.organisationUuid()) | |||
.setString(5, webhook.projectUuid()) | |||
.setLong(6, webhook.createdAt()) | |||
.setLong(7, webhook.createdAt()) | |||
.execute() | |||
.commit(); | |||
} else { | |||
LOGGER.info("Unable to migrate inconsistent webhook (entry deleted from PROPERTIES) : " + webhook); | |||
} | |||
} | |||
private List<Webhook> extractGlobalWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException { | |||
String defaultOrganizationUuid = defaultOrganizationUuidProvider.get(context); | |||
return Arrays.stream(values) | |||
.map(value -> new Webhook( | |||
properties.get("sonar.webhooks.global." + value + ".name"), | |||
properties.get("sonar.webhooks.global." + value + ".url"), | |||
defaultOrganizationUuid, null)) | |||
.collect(toList()); | |||
} | |||
private static List<Webhook> extractProjectWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException { | |||
List<Webhook> webhooks = new ArrayList<>(); | |||
for (String value : values) { | |||
PropertyRow name = properties.get("sonar.webhooks.project." + value + ".name"); | |||
PropertyRow url = properties.get("sonar.webhooks.project." + value + ".url"); | |||
webhooks.add(new Webhook(name, url, null, projectUuidOf(context, name))); | |||
} | |||
return webhooks; | |||
} | |||
private static String projectUuidOf(Context context, PropertyRow row) throws SQLException { | |||
return context | |||
.prepareSelect("select uuid from projects where id = ?") | |||
.setLong(1, row.resourceId()) | |||
.list(row1 -> row1.getString(1)).stream().findFirst().orElse(null); | |||
} | |||
private static class PropertyRow { | |||
private final Long id; | |||
private final String key; | |||
private final Long resourceId; | |||
private final String value; | |||
private final Long createdAt; | |||
public PropertyRow(long id, String key, Long resourceId, String value, Long createdAt) { | |||
this.id = id; | |||
this.key = key; | |||
this.resourceId = resourceId; | |||
this.value = value; | |||
this.createdAt = createdAt; | |||
} | |||
public Long id() { | |||
return id; | |||
} | |||
public String key() { | |||
return key; | |||
} | |||
public Long resourceId() { | |||
return resourceId; | |||
} | |||
public String value() { | |||
return value; | |||
} | |||
public Long createdAt() { | |||
return createdAt; | |||
} | |||
@Override | |||
public String toString() { | |||
return "{" + | |||
"id=" + id + | |||
", key='" + key + '\'' + | |||
", resourceId=" + resourceId + | |||
", value='" + value + '\'' + | |||
", createdAt=" + createdAt + | |||
'}'; | |||
} | |||
} | |||
private static class Webhook { | |||
private final PropertyRow name; | |||
private final PropertyRow url; | |||
private String organisationUuid; | |||
private String projectUuid; | |||
public Webhook(@Nullable PropertyRow name, @Nullable PropertyRow url, @Nullable String organisationUuid, @Nullable String projectUuid) { | |||
this.name = name; | |||
this.url = url; | |||
this.organisationUuid = organisationUuid; | |||
this.projectUuid = projectUuid; | |||
} | |||
public String name() { | |||
return name.value(); | |||
} | |||
public String url() { | |||
return url.value(); | |||
} | |||
public String organisationUuid() { | |||
return organisationUuid; | |||
} | |||
public String projectUuid() { | |||
return projectUuid; | |||
} | |||
public Long createdAt() { | |||
return name.createdAt(); | |||
} | |||
public boolean isValid() { | |||
return name != null && url != null && name() != null && url() != null && (organisationUuid() != null || projectUuid() != null) && createdAt() != null; | |||
} | |||
@Override | |||
public String toString() { | |||
final StringBuilder s = new StringBuilder().append("Webhook{").append("name=").append(name); | |||
if (name != null) { | |||
s.append(name.toString()); | |||
} | |||
s.append(", url=").append(url); | |||
if (url != null) { | |||
s.append(url.toString()); | |||
} | |||
s.append(", organisationUuid='").append(organisationUuid).append('\'') | |||
.append(", projectUuid='").append(projectUuid).append('\'').append('}'); | |||
return s.toString(); | |||
} | |||
} | |||
} |
@@ -36,7 +36,7 @@ public class DbVersion71Test { | |||
@Test | |||
public void verify_migration_count() { | |||
verifyMigrationCount(underTest, 14); | |||
verifyMigrationCount(underTest, 15); | |||
} | |||
} |
@@ -0,0 +1,205 @@ | |||
/* | |||
* 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.v71; | |||
import java.sql.SQLException; | |||
import java.util.Map; | |||
import javax.annotation.Nullable; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.api.utils.internal.TestSystem2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.core.util.UuidFactoryFast; | |||
import org.sonar.db.CoreDbTester; | |||
import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidProviderImpl; | |||
import static java.lang.Long.parseLong; | |||
import static java.lang.String.valueOf; | |||
import static org.apache.commons.lang.RandomStringUtils.randomNumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class MigrateWebhooksToWebhooksTableTest { | |||
@Rule | |||
public final CoreDbTester dbTester = CoreDbTester.createForSchema(MigrateWebhooksToWebhooksTableTest.class, "migrate_webhooks.sql"); | |||
private static final long NOW = 1_500_000_000_000L; | |||
private System2 system2 = new TestSystem2().setNow(NOW); | |||
private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); | |||
private MigrateWebhooksToWebhooksTable underTest = new MigrateWebhooksToWebhooksTable(dbTester.database(), new DefaultOrganizationUuidProviderImpl(), uuidFactory); | |||
@Test | |||
public void should_do_nothing_if_no_webhooks() throws SQLException { | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); | |||
} | |||
@Test | |||
public void should_migrate_one_global_webhook() throws SQLException { | |||
String uuid = insertDefaultOrganization(); | |||
insertProperty("sonar.webhooks.global", "1", null, system2.now()); | |||
insertProperty("sonar.webhooks.global.1.name", "a webhook", null, system2.now()); | |||
insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, system2.now()); | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); | |||
Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks"); | |||
assertThat(migrated.get("UUID")).isNotNull(); | |||
assertThat(migrated.get("NAME")).isEqualTo("a webhook"); | |||
assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); | |||
assertThat(migrated.get("PROJECT_UUID")).isNull(); | |||
assertThat(migrated.get("ORGANIZATION_UUID")).isEqualTo(uuid); | |||
assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); | |||
assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now()); | |||
assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now()); | |||
} | |||
@Test | |||
public void should_migrate_one_project_webhook() throws SQLException { | |||
String organization = insertDefaultOrganization(); | |||
String projectId = "156"; | |||
String projectUuid = UuidFactoryFast.getInstance().create(); | |||
; | |||
insertProject(organization, projectId, projectUuid); | |||
insertProperty("sonar.webhooks.project", "1", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now()); | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); | |||
Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks"); | |||
assertThat(migrated.get("UUID")).isNotNull(); | |||
assertThat(migrated.get("NAME")).isEqualTo("a webhook"); | |||
assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); | |||
assertThat(migrated.get("PROJECT_UUID")).isEqualTo(projectUuid); | |||
assertThat(migrated.get("ORGANIZATION_UUID")).isNull(); | |||
assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); | |||
assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now()); | |||
assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now()); | |||
} | |||
@Test | |||
public void should_migrate_global_webhooks() throws SQLException { | |||
insertDefaultOrganization(); | |||
insertProperty("sonar.webhooks.global", "1,2", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.1.name", "a webhook", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.2.url", "http://webhook.com", null, parseLong(randomNumeric(7))); | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2); | |||
} | |||
@Test | |||
public void should_migrate_only_valid_webhooks() throws SQLException { | |||
insertDefaultOrganization(); | |||
insertProperty("sonar.webhooks.global", "1,2,3,4", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.3.name", "a webhook", null, parseLong(randomNumeric(7))); | |||
insertProperty("sonar.webhooks.global.3.url", "http://webhook.com", null, parseLong(randomNumeric(7))); | |||
// nothing for 4 | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); | |||
} | |||
@Test | |||
public void should_migrate_project_webhooks() throws SQLException { | |||
String organization = insertDefaultOrganization(); | |||
String projectId = "156"; | |||
String projectUuid = UuidFactoryFast.getInstance().create(); | |||
; | |||
insertProject(organization, projectId, projectUuid); | |||
insertProperty("sonar.webhooks.project", "1,2", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.2.name", "another webhook", projectId, system2.now()); | |||
insertProperty("sonar.webhooks.project.2.url", "http://webhookhookhook.com", projectId, system2.now()); | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2); | |||
} | |||
@Test | |||
public void should_not_migrate_more_than_10_webhooks_per_project() throws SQLException { | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); | |||
} | |||
@Test | |||
public void should_not_migrate_more_than_10_global_webhooks() throws SQLException { | |||
underTest.execute(); | |||
assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); | |||
assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); | |||
} | |||
private void insertProperty(String key, @Nullable String value, @Nullable String resourceId, Long date) { | |||
dbTester.executeInsert("PROPERTIES", | |||
"id", randomNumeric(7), | |||
"prop_key", valueOf(key), | |||
"text_value", value, | |||
"is_empty", value.isEmpty() ? true : false, | |||
"resource_id", resourceId == null ? null : valueOf(resourceId), | |||
"created_at", valueOf(date)); | |||
} | |||
private String insertDefaultOrganization() { | |||
String uuid = UuidFactoryFast.getInstance().create(); | |||
dbTester.executeInsert( | |||
"INTERNAL_PROPERTIES", | |||
"KEE", "organization.default", | |||
"IS_EMPTY", "false", | |||
"TEXT_VALUE", uuid); | |||
return uuid; | |||
} | |||
private void insertProject(String organizationUuid, String projectId, String projectUuid) { | |||
dbTester.executeInsert( | |||
"PROJECTS", | |||
"ID", projectId, | |||
"ORGANIZATION_UUID", organizationUuid, | |||
"UUID", projectUuid); | |||
} | |||
} |
@@ -0,0 +1,76 @@ | |||
CREATE TABLE "INTERNAL_PROPERTIES" ( | |||
"KEE" VARCHAR(20) NOT NULL PRIMARY KEY, | |||
"IS_EMPTY" BOOLEAN NOT NULL, | |||
"TEXT_VALUE" VARCHAR(4000), | |||
"CLOB_VALUE" CLOB, | |||
"CREATED_AT" BIGINT | |||
); | |||
CREATE TABLE "PROJECTS" ( | |||
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"ORGANIZATION_UUID" VARCHAR(40) NOT NULL, | |||
"KEE" VARCHAR(400), | |||
"UUID" VARCHAR(50) NOT NULL, | |||
-- "UUID_PATH" VARCHAR(1500) NOT NULL, | |||
-- "ROOT_UUID" VARCHAR(50) NOT NULL, | |||
-- "PROJECT_UUID" VARCHAR(50) NOT NULL, | |||
"MODULE_UUID" VARCHAR(50), | |||
"MODULE_UUID_PATH" VARCHAR(1500), | |||
"MAIN_BRANCH_PROJECT_UUID" VARCHAR(50), | |||
"NAME" VARCHAR(2000), | |||
"DESCRIPTION" VARCHAR(2000), | |||
-- "PRIVATE" BOOLEAN NOT NULL, | |||
"TAGS" VARCHAR(500), | |||
-- "ENABLED" BOOLEAN NOT NULL DEFAULT TRUE, | |||
"SCOPE" VARCHAR(3), | |||
"QUALIFIER" VARCHAR(10), | |||
"DEPRECATED_KEE" VARCHAR(400), | |||
"PATH" VARCHAR(2000), | |||
"LANGUAGE" VARCHAR(20), | |||
"COPY_COMPONENT_UUID" VARCHAR(50), | |||
"LONG_NAME" VARCHAR(2000), | |||
"DEVELOPER_UUID" VARCHAR(50), | |||
"CREATED_AT" TIMESTAMP, | |||
"AUTHORIZATION_UPDATED_AT" BIGINT, | |||
"B_CHANGED" BOOLEAN, | |||
"B_COPY_COMPONENT_UUID" VARCHAR(50), | |||
"B_DESCRIPTION" VARCHAR(2000), | |||
"B_ENABLED" BOOLEAN, | |||
"B_UUID_PATH" VARCHAR(1500), | |||
"B_LANGUAGE" VARCHAR(20), | |||
"B_LONG_NAME" VARCHAR(500), | |||
"B_MODULE_UUID" VARCHAR(50), | |||
"B_MODULE_UUID_PATH" VARCHAR(1500), | |||
"B_NAME" VARCHAR(500), | |||
"B_PATH" VARCHAR(2000), | |||
"B_QUALIFIER" VARCHAR(10) | |||
); | |||
CREATE TABLE "PROPERTIES" ( | |||
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"PROP_KEY" VARCHAR(512) NOT NULL, | |||
"RESOURCE_ID" INTEGER, | |||
"USER_ID" INTEGER, | |||
"IS_EMPTY" BOOLEAN NOT NULL, | |||
"TEXT_VALUE" VARCHAR(4000), | |||
"CLOB_VALUE" CLOB, | |||
"CREATED_AT" BIGINT | |||
); | |||
CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES" ("PROP_KEY"); | |||
CREATE TABLE "WEBHOOKS" ( | |||
"UUID" VARCHAR(40) NOT NULL PRIMARY KEY, | |||
"NAME" VARCHAR(100) NOT NULL, | |||
"URL" VARCHAR(2000) NOT NULL, | |||
"ORGANIZATION_UUID" VARCHAR(40), | |||
"PROJECT_UUID" VARCHAR(40), | |||
"CREATED_AT" BIGINT NOT NULL, | |||
"UPDATED_AT" BIGINT | |||
); | |||
CREATE UNIQUE INDEX "PK_WEBHOOKS" ON "WEBHOOKS" ("UUID"); | |||
CREATE INDEX "ORGANIZATION_WEBHOOK" ON "WEBHOOKS" ("ORGANIZATION_UUID"); | |||
CREATE INDEX "PROJECT_WEBHOOK" ON "WEBHOOKS" ("PROJECT_UUID"); | |||
@@ -35,16 +35,10 @@ public interface WebHooks { | |||
* | |||
* <p> | |||
* This can be used to not do consuming operations before calling | |||
* {@link #sendProjectAnalysisUpdate(ComponentDto, Analysis, Supplier)} | |||
* {@link #sendProjectAnalysisUpdate(Analysis, Supplier)} | |||
*/ | |||
boolean isEnabled(ComponentDto projectDto); | |||
/** | |||
* Calls all WebHooks configured in the specified {@link Configuration} for the specified analysis with the | |||
* {@link WebhookPayload} provided by the specified Supplier. | |||
*/ | |||
void sendProjectAnalysisUpdate(ComponentDto projectDto, Analysis analysis, Supplier<WebhookPayload> payloadSupplier); | |||
/** | |||
* Calls all WebHooks configured in the specified {@link Configuration} for the specified analysis with the | |||
* {@link WebhookPayload} provided by the specified Supplier. |
@@ -29,7 +29,6 @@ import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.webhook.WebhookDao; | |||
import org.sonar.db.webhook.WebhookDto; | |||
import org.sonar.server.async.AsyncExecution; | |||
@@ -55,25 +54,25 @@ public class WebHooksImpl implements WebHooks { | |||
@Override | |||
public boolean isEnabled(ComponentDto projectDto) { | |||
return readWebHooksFrom(projectDto) | |||
return readWebHooksFrom(projectDto.uuid()) | |||
.findAny() | |||
.isPresent(); | |||
} | |||
private Stream<WebhookDto> readWebHooksFrom(ComponentDto projectDto) { | |||
private Stream<WebhookDto> readWebHooksFrom(String projectUuid) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Optional<OrganizationDto> optionalDto = dbClient.organizationDao().selectByUuid(dbSession, projectDto.getOrganizationUuid()); | |||
OrganizationDto organizationDto = checkStateWithOptional(optionalDto, "the requested organization '%s' was not found", projectDto.getOrganizationUuid()); | |||
Optional<ComponentDto> componentDto = ofNullable(dbClient.componentDao().selectByUuid(dbSession, projectUuid).orNull()); | |||
ComponentDto projectDto = checkStateWithOptional(componentDto, "the requested project '%s' was not found", projectUuid); | |||
WebhookDao dao = dbClient.webhookDao(); | |||
return Stream.concat( | |||
dao.selectByProjectUuid(dbSession, projectDto).stream(), | |||
dao.selectByOrganizationUuid(dbSession, organizationDto).stream()); | |||
dao.selectByProject(dbSession, projectDto).stream(), | |||
dao.selectByOrganizationUuid(dbSession, projectDto.getOrganizationUuid()).stream()); | |||
} | |||
} | |||
@Override | |||
public void sendProjectAnalysisUpdate(ComponentDto componentDto, Analysis analysis, Supplier<WebhookPayload> payloadSupplier) { | |||
List<Webhook> webhooks = readWebHooksFrom(componentDto) | |||
public void sendProjectAnalysisUpdate(Analysis analysis, Supplier<WebhookPayload> payloadSupplier) { | |||
List<Webhook> webhooks = readWebHooksFrom(analysis.getProjectUuid()) | |||
.map(dto -> new Webhook(analysis.getProjectUuid(), analysis.getCeTaskUuid(), analysis.getAnalysisUuid(), dto.getName(), dto.getUrl())) | |||
.collect(MoreCollectors.toList()); | |||
if (webhooks.isEmpty()) { | |||
@@ -89,15 +88,6 @@ public class WebHooksImpl implements WebHooks { | |||
asyncExecution.addToQueue(() -> deliveryStorage.purge(analysis.getProjectUuid())); | |||
} | |||
@Override | |||
public void sendProjectAnalysisUpdate(Analysis analysis, Supplier<WebhookPayload> payloadSupplier) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Optional<ComponentDto> optionalDto = ofNullable(dbClient.componentDao().selectByUuid(dbSession, analysis.getProjectUuid()).orNull()); | |||
ComponentDto projectDto = checkStateWithOptional(optionalDto, "the requested project '%s' was not found", analysis.getProjectUuid()); | |||
sendProjectAnalysisUpdate(projectDto, analysis, payloadSupplier); | |||
} | |||
} | |||
private static void log(WebhookDelivery delivery) { | |||
Optional<String> error = delivery.getErrorMessage(); | |||
if (error.isPresent()) { |
@@ -79,7 +79,6 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener { | |||
private void callWebhook(DbSession dbSession, QGChangeEvent event, @Nullable EvaluatedQualityGate evaluatedQualityGate) { | |||
webhooks.sendProjectAnalysisUpdate( | |||
event.getProject(), | |||
new WebHooks.Analysis(event.getBranch().getUuid(), event.getAnalysis().getUuid(), null), | |||
() -> buildWebHookPayload(dbSession, event, evaluatedQualityGate)); | |||
} |
@@ -198,11 +198,11 @@ public class CreateAction implements WebhooksWsAction { | |||
} | |||
private int numberOfWebhookOf(DbSession dbSession, OrganizationDto organizationDto) { | |||
return dbClient.webhookDao().selectByOrganizationUuid(dbSession, organizationDto).size(); | |||
return dbClient.webhookDao().selectByOrganization(dbSession, organizationDto).size(); | |||
} | |||
private int numberOfWebhookOf(DbSession dbSession, ComponentDto componentDto) { | |||
return dbClient.webhookDao().selectByProjectUuid(dbSession, componentDto).size(); | |||
return dbClient.webhookDao().selectByProject(dbSession, componentDto).size(); | |||
} | |||
private OrganizationDto defaultOrganizationDto(DbSession dbSession) { |
@@ -33,7 +33,7 @@ import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.webhook.WebhookDto; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Webhooks.SearchWsResponse.Builder; | |||
import org.sonarqube.ws.Webhooks.ListWsResponse.Builder; | |||
import static java.util.Optional.ofNullable; | |||
import static org.apache.commons.lang.StringUtils.isNotBlank; | |||
@@ -45,7 +45,7 @@ import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; | |||
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; | |||
import static org.sonar.server.ws.WsUtils.checkStateWithOptional; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
import static org.sonarqube.ws.Webhooks.SearchWsResponse.newBuilder; | |||
import static org.sonarqube.ws.Webhooks.ListWsResponse.newBuilder; | |||
public class ListAction implements WebhooksWsAction { | |||
@@ -115,12 +115,12 @@ public class ListAction implements WebhooksWsAction { | |||
webhookSupport.checkPermission(componentDto); | |||
webhookSupport.checkThatProjectBelongsToOrganization(componentDto, organizationDto, "Project '%s' does not belong to organisation '%s'", projectKey, organizationKey); | |||
webhookSupport.checkPermission(componentDto); | |||
return dbClient.webhookDao().selectByProjectUuid(dbSession, componentDto); | |||
return dbClient.webhookDao().selectByProject(dbSession, componentDto); | |||
} else { | |||
webhookSupport.checkPermission(organizationDto); | |||
return dbClient.webhookDao().selectByOrganizationUuid(dbSession, organizationDto); | |||
return dbClient.webhookDao().selectByOrganization(dbSession, organizationDto); | |||
} | |||
@@ -32,7 +32,7 @@ public class WebhookSupport { | |||
private final UserSession userSession; | |||
WebhookSupport(UserSession userSession) { | |||
public WebhookSupport(UserSession userSession) { | |||
this.userSession = userSession; | |||
} | |||
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.organization.ws; | |||
import java.util.List; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
@@ -39,6 +40,8 @@ import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.webhook.WebhookDbTester; | |||
import org.sonar.db.webhook.WebhookDto; | |||
import org.sonar.server.component.ComponentCleanerService; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.es.ProjectIndexers; | |||
@@ -48,7 +51,6 @@ import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestOrganizationFlags; | |||
import org.sonar.server.qualitygate.QualityGateFinder; | |||
import org.sonar.server.qualityprofile.QProfileFactory; | |||
import org.sonar.server.qualityprofile.QProfileFactoryImpl; | |||
import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; | |||
@@ -90,7 +92,7 @@ public class DeleteActionTest { | |||
private QProfileFactory qProfileFactory = new QProfileFactoryImpl(dbClient, mock(UuidFactory.class), System2.INSTANCE, mock(ActiveRuleIndexer.class)); | |||
private UserIndex userIndex = new UserIndex(es.client(), System2.INSTANCE); | |||
private UserIndexer userIndexer = new UserIndexer(dbClient, es.client()); | |||
private QualityGateFinder qualityGateFinder = new QualityGateFinder(dbClient); | |||
private final WebhookDbTester webhookDbTester = db.webhooks(); | |||
private WsActionTester wsTester = new WsActionTester( | |||
new DeleteAction(userSession, dbClient, defaultOrganizationProvider, componentCleanerService, organizationFlags, userIndexer, qProfileFactory)); | |||
@@ -113,6 +115,23 @@ public class DeleteActionTest { | |||
.matches(param -> "Organization key".equals(param.description())); | |||
} | |||
@Test | |||
public void organization_deletion_also_ensure_that_webhooks_of_this_organization_if_they_exist_are_cleared() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
webhookDbTester.insertWebhook(organization); | |||
webhookDbTester.insertWebhook(organization); | |||
webhookDbTester.insertWebhook(organization); | |||
userSession.logIn().addPermission(ADMINISTER, organization); | |||
wsTester.newRequest() | |||
.setParam(PARAM_ORGANIZATION, organization.getKey()) | |||
.execute(); | |||
List<WebhookDto> webhookDtos = dbClient.webhookDao().selectByOrganization(session, organization); | |||
assertThat(webhookDtos).isEmpty(); | |||
} | |||
@Test | |||
public void organization_deletion_also_ensure_that_homepage_on_this_organization_if_it_exists_is_cleared() { | |||
OrganizationDto organization = db.organizations().insert(); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.project.ws; | |||
import java.util.List; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
@@ -33,6 +34,8 @@ import org.sonar.db.component.ComponentDbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.ResourceTypesRule; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.webhook.WebhookDbTester; | |||
import org.sonar.db.webhook.WebhookDto; | |||
import org.sonar.server.component.ComponentCleanerService; | |||
import org.sonar.server.es.TestProjectIndexers; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
@@ -71,6 +74,7 @@ public class DeleteActionTest { | |||
private WsTester ws; | |||
private DbClient dbClient = db.getDbClient(); | |||
private DbSession dbSession = db.getSession(); | |||
private WebhookDbTester webhookDbTester = db.webhooks(); | |||
private ComponentDbTester componentDbTester = new ComponentDbTester(db); | |||
private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class); | |||
@@ -136,7 +140,6 @@ public class DeleteActionTest { | |||
@Test | |||
public void project_deletion_also_ensure_that_homepage_on_this_project_if_it_exists_is_cleared() throws Exception { | |||
ComponentDto project = componentDbTester.insertPrivateProject(); | |||
UserDto insert = dbClient.userDao().insert(dbSession, | |||
newUserDto().setHomepageType("PROJECT").setHomepageParameter(project.uuid())); | |||
@@ -157,6 +160,28 @@ public class DeleteActionTest { | |||
assertThat(userReloaded.getHomepageParameter()).isNull(); | |||
} | |||
@Test | |||
public void project_deletion_also_ensure_that_webhooks_on_this_project_if_they_exists_are_deleted() throws Exception { | |||
ComponentDto project = componentDbTester.insertPrivateProject(); | |||
webhookDbTester.insertWebhook(project); | |||
webhookDbTester.insertWebhook(project); | |||
webhookDbTester.insertWebhook(project); | |||
webhookDbTester.insertWebhook(project); | |||
userSessionRule.logIn().addProjectPermission(ADMIN, project); | |||
new WsTester(new ProjectsWs( | |||
new DeleteAction( | |||
new ComponentCleanerService(dbClient, new ResourceTypesRule().setAllQualifiers(PROJECT), | |||
new TestProjectIndexers()), from(db), dbClient, userSessionRule))) | |||
.newPostRequest(CONTROLLER, ACTION) | |||
.setParam(PARAM_PROJECT, project.getDbKey()) | |||
.execute(); | |||
List<WebhookDto> webhookDtos = dbClient.webhookDao().selectByProject(dbSession, project); | |||
assertThat(webhookDtos).isEmpty(); | |||
} | |||
@Test | |||
public void return_403_if_not_project_admin_nor_org_admin() throws Exception { | |||
ComponentDto project = componentDbTester.insertPrivateProject(); |
@@ -38,8 +38,8 @@ import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Webhooks.SearchWsResponse; | |||
import org.sonarqube.ws.Webhooks.SearchWsResponse.Search; | |||
import org.sonarqube.ws.Webhooks.ListWsResponse; | |||
import org.sonarqube.ws.Webhooks.ListWsResponse.List; | |||
import static java.lang.String.format; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@@ -92,24 +92,24 @@ public class ListActionTest { | |||
} | |||
@Test | |||
public void search_global_webhooks() { | |||
public void List_global_webhooks() { | |||
WebhookDto dto1 = webhookDbTester.insertWebhook(db.getDefaultOrganization()); | |||
WebhookDto dto2 = webhookDbTester.insertWebhook(db.getDefaultOrganization()); | |||
userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization().getUuid()); | |||
SearchWsResponse response = wsActionTester.newRequest() | |||
.executeProtobuf(SearchWsResponse.class); | |||
ListWsResponse response = wsActionTester.newRequest() | |||
.executeProtobuf(ListWsResponse.class); | |||
assertThat(response.getWebhooksList()) | |||
.extracting(Search::getName, Search::getUrl) | |||
.extracting(List::getName, List::getUrl) | |||
.contains(tuple(dto1.getName(), dto1.getUrl()), | |||
tuple(dto2.getName(), dto2.getUrl())); | |||
} | |||
@Test | |||
public void search_project_webhooks_when_no_organization_is_provided() { | |||
public void List_project_webhooks_when_no_organization_is_provided() { | |||
ComponentDto project1 = componentDbTester.insertPrivateProject(); | |||
userSession.logIn().addProjectPermission(ADMIN, project1); | |||
@@ -117,38 +117,38 @@ public class ListActionTest { | |||
WebhookDto dto1 = webhookDbTester.insertWebhook(project1); | |||
WebhookDto dto2 = webhookDbTester.insertWebhook(project1); | |||
SearchWsResponse response = wsActionTester.newRequest() | |||
ListWsResponse response = wsActionTester.newRequest() | |||
.setParam(PROJECT_KEY_PARAM, project1.getKey()) | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
assertThat(response.getWebhooksList()) | |||
.extracting(Search::getName, Search::getUrl) | |||
.extracting(List::getName, List::getUrl) | |||
.contains(tuple(dto1.getName(), dto1.getUrl()), | |||
tuple(dto2.getName(), dto2.getUrl())); | |||
} | |||
@Test | |||
public void search_organization_webhooks() { | |||
public void List_organization_webhooks() { | |||
OrganizationDto organizationDto = organizationDbTester.insert(); | |||
WebhookDto dto1 = webhookDbTester.insertWebhook(organizationDto); | |||
WebhookDto dto2 = webhookDbTester.insertWebhook(organizationDto); | |||
userSession.logIn().addPermission(ADMINISTER, organizationDto.getUuid()); | |||
SearchWsResponse response = wsActionTester.newRequest() | |||
ListWsResponse response = wsActionTester.newRequest() | |||
.setParam(ORGANIZATION_KEY_PARAM, organizationDto.getKey()) | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
assertThat(response.getWebhooksList()) | |||
.extracting(Search::getName, Search::getUrl) | |||
.extracting(List::getName, List::getUrl) | |||
.contains(tuple(dto1.getName(), dto1.getUrl()), | |||
tuple(dto2.getName(), dto2.getUrl())); | |||
} | |||
@Test | |||
public void search_project_webhooks_when_organization_is_provided() { | |||
public void List_project_webhooks_when_organization_is_provided() { | |||
OrganizationDto organization = organizationDbTester.insert(); | |||
ComponentDto project = componentDbTester.insertPrivateProject(organization); | |||
@@ -157,13 +157,13 @@ public class ListActionTest { | |||
WebhookDto dto1 = webhookDbTester.insertWebhook(project); | |||
WebhookDto dto2 = webhookDbTester.insertWebhook(project); | |||
SearchWsResponse response = wsActionTester.newRequest() | |||
ListWsResponse response = wsActionTester.newRequest() | |||
.setParam(ORGANIZATION_KEY_PARAM, organization.getKey()) | |||
.setParam(PROJECT_KEY_PARAM, project.getKey()) | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
assertThat(response.getWebhooksList()) | |||
.extracting(Search::getName, Search::getUrl) | |||
.extracting(List::getName, List::getUrl) | |||
.contains(tuple(dto1.getName(), dto1.getUrl()), | |||
tuple(dto2.getName(), dto2.getUrl())); | |||
@@ -177,7 +177,7 @@ public class ListActionTest { | |||
wsActionTester.newRequest() | |||
.setParam(PROJECT_KEY_PARAM, "pipo") | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
} | |||
@@ -189,7 +189,7 @@ public class ListActionTest { | |||
wsActionTester.newRequest() | |||
.setParam(ORGANIZATION_KEY_PARAM, "pipo") | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
} | |||
@@ -218,7 +218,7 @@ public class ListActionTest { | |||
expectedException.expect(UnauthorizedException.class); | |||
wsActionTester.newRequest() | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
} | |||
@@ -231,7 +231,7 @@ public class ListActionTest { | |||
expectedException.expectMessage("Insufficient privileges"); | |||
wsActionTester.newRequest() | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
} | |||
@Test | |||
@@ -246,7 +246,7 @@ public class ListActionTest { | |||
wsActionTester.newRequest() | |||
.setParam(PROJECT_KEY_PARAM, project.getKey()) | |||
.executeProtobuf(SearchWsResponse.class); | |||
.executeProtobuf(ListWsResponse.class); | |||
} | |||
@@ -24,11 +24,11 @@ option java_package = "org.sonarqube.ws"; | |||
option java_outer_classname = "Webhooks"; | |||
option optimize_for = SPEED; | |||
// GET api/webhooks/search | |||
message SearchWsResponse { | |||
repeated Search webhooks = 1; | |||
// GET api/webhooks/list | |||
message ListWsResponse { | |||
repeated List webhooks = 1; | |||
message Search { | |||
message List { | |||
optional string key = 1; | |||
optional string name = 2; | |||
optional string url = 3; |