From 8301fe8e2841fdb94f12349134f23939e2e057c0 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Tue, 8 Nov 2016 20:43:56 +0100 Subject: [PATCH] SONAR-8351 Send JSON payload over HTTP when project analysis is complete --- .../ComputeEngineContainerImplTest.java | 2 +- ...ProjectAnalysisTaskContainerPopulator.java | 6 +- .../task/projectanalysis/webhook/Webhook.java | 44 ++++++ .../webhook/WebhookCaller.java | 34 +++++ .../webhook/WebhookCallerImpl.java | 69 +++++++++ .../webhook/WebhookDelivery.java | 131 ++++++++++++++++ .../webhook/WebhookModule.java | 31 ++++ .../webhook/WebhookPayload.java | 107 +++++++++++++ .../webhook/WebhookPostTask.java | 96 ++++++++++++ .../projectanalysis/webhook/package-info.java | 23 +++ .../component/TestSettingsRepository.java | 40 +++++ .../webhook/TestWebhookCaller.java | 75 +++++++++ .../webhook/WebhookCallerImplTest.java | 92 ++++++++++++ .../webhook/WebhookPayloadTest.java | 142 ++++++++++++++++++ .../webhook/WebhookPostTaskTest.java | 107 +++++++++++++ .../webhook/WebhookPayloadTest/failed.json | 8 + .../gate_condition_without_value.json | 23 +++ .../webhook/WebhookPayloadTest/success.json | 24 +++ .../core/config/CorePropertyDefinitions.java | 1 + .../sonar/core/config/WebhookProperties.java | 77 ++++++++++ .../resources/org/sonar/l10n/core.properties | 1 + .../config/CorePropertyDefinitionsTest.java | 2 +- 22 files changed, 1132 insertions(+), 3 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json create mode 100644 sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index b9b59c7dc48..b9d1c12bfef 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -108,7 +108,7 @@ public class ComputeEngineContainerImplTest { + 25 // level 1 + 46 // content of DaoModule + 2 // content of EsSearchModule - + 62 // content of CorePropertyDefinitions + + 64 // content of CorePropertyDefinitions + 1 // content of CePropertyDefinitions ); assertThat(picoContainer.getParent().getParent().getParent().getParent()).isNull(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 0431c0983ae..3c32129608d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -101,6 +101,7 @@ import org.sonar.server.computation.task.projectanalysis.source.LastCommitVisito import org.sonar.server.computation.task.projectanalysis.source.SourceHashRepositoryImpl; import org.sonar.server.computation.task.projectanalysis.source.SourceLinesRepositoryImpl; import org.sonar.server.computation.task.projectanalysis.step.ReportComputationSteps; +import org.sonar.server.computation.task.projectanalysis.webhook.WebhookModule; import org.sonar.server.computation.task.step.ComputationStepExecutor; import org.sonar.server.computation.task.step.ComputationSteps; import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl; @@ -236,7 +237,10 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop // views ViewIndex.class, - MeasureToMeasureDto.class); + MeasureToMeasureDto.class, + + // webhooks + WebhookModule.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java new file mode 100644 index 00000000000..18c1de9ff67 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import javax.annotation.concurrent.Immutable; + +import static java.util.Objects.requireNonNull; + +@Immutable +public class Webhook { + + private final String name; + private final String url; + + public Webhook(String name, String url) { + this.name = requireNonNull(name); + this.url = requireNonNull(url); + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java new file mode 100644 index 00000000000..b9316735ed5 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +public interface WebhookCaller { + + /** + * Call webhook by sending a HTTP(S) POST request containing + * the JSON payload. + *
+ * Errors are silently ignored. They don't generate logs or + * throw exceptions. The error status is stored in the + * returned {@link WebhookDelivery}. + */ + WebhookDelivery call(Webhook webhook, WebhookPayload payload); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java new file mode 100644 index 00000000000..26f618bb2bd --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.utils.System2; + +@ComputeEngineSide +public class WebhookCallerImpl implements WebhookCaller { + + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static final String PROJECT_KEY_HEADER = "X-SonarQube-Project"; + + private final System2 system; + private final OkHttpClient okHttpClient; + + public WebhookCallerImpl(System2 system, OkHttpClient okHttpClient) { + this.system = system; + this.okHttpClient = okHttpClient; + } + + @Override + public WebhookDelivery call(Webhook webhook, WebhookPayload payload) { + Request.Builder request = new Request.Builder(); + request.url(webhook.getUrl()); + request.header(PROJECT_KEY_HEADER, payload.getProjectKey()); + RequestBody body = RequestBody.create(JSON, payload.toJson()); + request.post(body); + + WebhookDelivery.Builder builder = new WebhookDelivery.Builder(); + long startedAt = system.now(); + builder + .setAt(startedAt) + .setPayload(payload) + .setWebhook(webhook); + try { + Response response = okHttpClient.newCall(request.build()).execute(); + builder.setHttpStatus(response.code()); + builder.setDurationInMs(system.now() - startedAt); + } catch (IOException e) { + builder.setThrowable(e); + } + return builder.build(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java new file mode 100644 index 00000000000..a2623450e82 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.util.Optional; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link WebhookDelivery} represents the result of a webhook call. + */ +@Immutable +public class WebhookDelivery { + + private final Webhook webhook; + private final WebhookPayload payload; + private final Integer httpStatus; + private final Long durationInMs; + private final long at; + private final Throwable throwable; + + private WebhookDelivery(Builder builder) { + this.webhook = requireNonNull(builder.webhook); + this.payload = requireNonNull(builder.payload); + this.httpStatus = builder.httpStatus; + this.durationInMs = builder.durationInMs; + this.at = builder.at; + this.throwable = builder.throwable; + } + + public Webhook getWebhook() { + return webhook; + } + + public WebhookPayload getPayload() { + return payload; + } + + /** + * @return the HTTP status if {@link #getThrowable()} is empty, else returns + * {@link Optional#empty()} + */ + public Optional getHttpStatus() { + return Optional.ofNullable(httpStatus); + } + + /** + * @return the duration in milliseconds if {@link #getThrowable()} is empty, + * else returns {@link Optional#empty()} + */ + public Optional getDurationInMs() { + return Optional.ofNullable(durationInMs); + } + + /** + * @return the date of sending + */ + public long getAt() { + return at; + } + + /** + * @return the error raised if the request could not be executed due to a connectivity + * problem or timeout + */ + public Optional getThrowable() { + return Optional.ofNullable(throwable); + } + + public static class Builder { + private Webhook webhook; + private WebhookPayload payload; + private Integer httpStatus; + private Long durationInMs; + private long at; + private Throwable throwable; + + public Builder setWebhook(Webhook w) { + this.webhook = w; + return this; + } + + public Builder setPayload(WebhookPayload payload) { + this.payload = payload; + return this; + } + + public Builder setHttpStatus(@Nullable Integer httpStatus) { + this.httpStatus = httpStatus; + return this; + } + + public Builder setDurationInMs(@Nullable Long durationInMs) { + this.durationInMs = durationInMs; + return this; + } + + public Builder setAt(long at) { + this.at = at; + return this; + } + + public Builder setThrowable(@Nullable Throwable t) { + this.throwable = t; + return this; + } + + public WebhookDelivery build() { + return new WebhookDelivery(this); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java new file mode 100644 index 00000000000..1bc200acdc6 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import org.sonar.core.platform.Module; + +public class WebhookModule extends Module { + @Override + protected void configureModule() { + add( + WebhookCallerImpl.class, + WebhookPostTask.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java new file mode 100644 index 00000000000..2429b1236d8 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.io.StringWriter; +import java.io.Writer; +import java.util.Date; +import java.util.Optional; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.ce.posttask.CeTask; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; +import org.sonar.api.ce.posttask.Project; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.utils.text.JsonWriter; + +import static java.util.Objects.requireNonNull; + +@Immutable +public class WebhookPayload { + + private final String projectKey; + private final String json; + + public WebhookPayload(String projectKey, String json) { + this.projectKey = requireNonNull(projectKey); + this.json = requireNonNull(json); + } + + public String getProjectKey() { + return projectKey; + } + + public String toJson() { + return json; + } + + public static WebhookPayload from(PostProjectAnalysisTask.ProjectAnalysis analysis) { + Writer string = new StringWriter(); + JsonWriter writer = JsonWriter.of(string); + writer.beginObject(); + writeTask(writer, analysis.getCeTask()); + Optional analysisDate = analysis.getAnalysisDate(); + if (analysisDate.isPresent()) { + writer.propDateTime("analysedAt", analysisDate.get()); + } + writeProject(analysis, writer, analysis.getProject()); + writeQualityGate(writer, analysis.getQualityGate()); + writer.endObject().close(); + return new WebhookPayload(analysis.getProject().getKey(), string.toString()); + } + + private static void writeTask(JsonWriter writer, CeTask ceTask) { + writer.prop("taskId", ceTask.getId()); + writer.prop("status", ceTask.getStatus().toString()); + } + + private static void writeProject(PostProjectAnalysisTask.ProjectAnalysis analysis, JsonWriter writer, Project project) { + writer.name("project"); + writer.beginObject(); + writer.prop("key", project.getKey()); + writer.prop("name", analysis.getProject().getName()); + writer.endObject(); + } + + private static void writeQualityGate(JsonWriter writer, @Nullable QualityGate gate) { + if (gate != null) { + writer.name("qualityGate"); + writer.beginObject(); + writer.prop("name", gate.getName()); + writer.prop("status", gate.getStatus().toString()); + writer.name("conditions").beginArray(); + for (QualityGate.Condition condition : gate.getConditions()) { + writer.beginObject(); + writer.prop("metric", condition.getMetricKey()); + writer.prop("operator", condition.getOperator().name()); + if (condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) { + writer.prop("value", condition.getValue()); + } + writer.prop("status", condition.getStatus().name()); + writer.prop("onLeakPeriod", condition.isOnLeakPeriod()); + writer.prop("errorThreshold", condition.getErrorThreshold()); + writer.prop("warningThreshold", condition.getWarningThreshold()); + writer.endObject(); + } + writer.endArray(); + writer.endObject(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java new file mode 100644 index 00000000000..8ec33dd1eac --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import com.google.common.collect.Iterables; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.config.WebhookProperties; +import org.sonar.core.util.stream.Collectors; +import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository; +import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder; + +import static com.google.common.base.Throwables.getRootCause; +import static java.lang.String.format; + +public class WebhookPostTask implements PostProjectAnalysisTask { + + private static final Logger LOGGER = Loggers.get(WebhookPostTask.class); + + private final TreeRootHolder rootHolder; + private final SettingsRepository settingsRepository; + private final WebhookCaller caller; + + public WebhookPostTask(TreeRootHolder rootHolder, SettingsRepository settingsRepository, WebhookCaller caller) { + this.rootHolder = rootHolder; + this.settingsRepository = settingsRepository; + this.caller = caller; + } + + @Override + public void finished(ProjectAnalysis analysis) { + Settings settings = settingsRepository.getSettings(rootHolder.getRoot()); + + Iterable webhookProps = Iterables.concat( + getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY), + getWebhookProperties(settings, WebhookProperties.PROJECT_KEY) + ); + if (!Iterables.isEmpty(webhookProps)) { + process(settings, analysis, webhookProps); + } + } + + private static List getWebhookProperties(Settings settings, String propertyKey) { + String[] webhookIds = settings.getStringArray(propertyKey); + return Arrays.stream(webhookIds) + .map(webhookId -> format("%s.%s", propertyKey, webhookId)) + .collect(Collectors.toList(webhookIds.length)); + } + + private void process(Settings settings, ProjectAnalysis analysis, Iterable webhookProperties) { + WebhookPayload payload = WebhookPayload.from(analysis); + for (String webhookProp : webhookProperties) { + String name = settings.getString(format("%s.%s", webhookProp, WebhookProperties.NAME_FIELD)); + String url = settings.getString(format("%s.%s", webhookProp, WebhookProperties.URL_FIELD)); + // as webhooks are defined as property sets, we can't ensure validity of fields on creation. + if (name != null && url != null) { + Webhook webhook = new Webhook(name, url); + WebhookDelivery delivery = caller.call(webhook, payload); + log(delivery); + } + } + } + + private static void log(WebhookDelivery delivery) { + Optional throwable = delivery.getThrowable(); + if (throwable.isPresent()) { + LOGGER.debug("Failed to send webhook '{}' | url={} | message={}", + delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), getRootCause(throwable.get()).getMessage()); + } else { + LOGGER.debug("Sent webhook '{}' | url={} | time={}ms | status={}", + delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), delivery.getDurationInMs().orElse(-1L), delivery.getHttpStatus().orElse(-1)); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java new file mode 100644 index 00000000000..7ef8477a244 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.computation.task.projectanalysis.webhook; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java new file mode 100644 index 00000000000..1973ebb7576 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.component; + +import org.sonar.api.config.Settings; + +/** + * Implementation of {@link SettingsRepository} that always return the + * same mutable {@link Settings}, whatever the component. + */ +public class TestSettingsRepository implements SettingsRepository { + + private final Settings settings; + + public TestSettingsRepository(Settings settings) { + this.settings = settings; + } + + @Override + public Settings getSettings(Component component) { + return settings; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java new file mode 100644 index 00000000000..1e82369282d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +public class TestWebhookCaller implements WebhookCaller { + + private final Queue deliveries = new LinkedList<>(); + private final AtomicInteger countSent = new AtomicInteger(0); + + public TestWebhookCaller enqueueSuccess(long at, int httpCode, long durationMs) { + deliveries.add(new Item(at, httpCode, durationMs, null)); + return this; + } + + public TestWebhookCaller enqueueFailure(long at, Throwable t) { + deliveries.add(new Item(at, null, null, t)); + return this; + } + + @Override + public WebhookDelivery call(Webhook webhook, WebhookPayload payload) { + Item item = requireNonNull(deliveries.poll(), "Queue is empty"); + countSent.incrementAndGet(); + return new WebhookDelivery.Builder() + .setAt(item.at) + .setHttpStatus(item.httpCode) + .setDurationInMs(item.durationMs) + .setThrowable(item.throwable) + .setPayload(payload) + .setWebhook(webhook) + .build(); + } + + public int countSent() { + return countSent.get(); + } + + private static class Item { + final long at; + final Integer httpCode; + final Long durationMs; + final Throwable throwable; + + Item(long at, @Nullable Integer httpCode, @Nullable Long durationMs, @Nullable Throwable throwable) { + this.at = at; + this.httpCode = httpCode; + this.durationMs = durationMs; + this.throwable = throwable; + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java new file mode 100644 index 00000000000..474974fa5d1 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.config.MapSettings; +import org.sonar.api.internal.SonarRuntimeImpl; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.server.util.OkHttpClientProvider; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class WebhookCallerImplTest { + + private static final long NOW = 1_500_000_000_000L; + + @Rule + public MockWebServer server = new MockWebServer(); + + private System2 system = new TestSystem2().setNow(NOW); + + @Test + public void send_posts_payload_to_http_server() throws Exception { + Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString()); + WebhookPayload payload = new WebhookPayload("P1", "{the payload}"); + + server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); + WebhookDelivery delivery = newSender().call(webhook, payload); + + assertThat(delivery.getHttpStatus().get()).isEqualTo(201); + assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0L); + assertThat(delivery.getThrowable()).isEmpty(); + assertThat(delivery.getAt()).isEqualTo(NOW); + assertThat(delivery.getWebhook()).isSameAs(webhook); + assertThat(delivery.getPayload()).isSameAs(payload); + + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("POST"); + assertThat(recordedRequest.getPath()).isEqualTo("/ping"); + assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(payload.toJson()); + assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2"); + assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo("application/json; charset=utf-8"); + assertThat(recordedRequest.getHeader("X-SonarQube-Project")).isEqualTo(payload.getProjectKey()); + } + + @Test + public void send_does_not_throw_exception_on_errors() throws Exception { + Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString()); + WebhookPayload payload = new WebhookPayload("P1", "{the payload}"); + + server.shutdown(); + WebhookDelivery delivery = newSender().call(webhook, payload); + + assertThat(delivery.getHttpStatus()).isEmpty(); + assertThat(delivery.getDurationInMs()).isEmpty(); + assertThat(delivery.getThrowable().get().getMessage()).startsWith("Failed to connect to"); + assertThat(delivery.getAt()).isEqualTo(NOW); + assertThat(delivery.getWebhook()).isSameAs(webhook); + assertThat(delivery.getPayload()).isSameAs(payload); + } + + private WebhookCaller newSender() { + SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER); + return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings(), runtime)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java new file mode 100644 index 00000000000..f01d81d401d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java @@ -0,0 +1,142 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.util.Date; +import java.util.Optional; +import javax.annotation.Nullable; +import org.junit.Test; +import org.sonar.api.ce.posttask.CeTask; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; +import org.sonar.api.ce.posttask.Project; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.ce.posttask.ScannerContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newConditionBuilder; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newQualityGateBuilder; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder; +import static org.sonar.test.JsonAssert.assertJson; + +public class WebhookPayloadTest { + + private static final String PROJECT_KEY = "P1"; + + @Test + public void create_payload_for_successful_analysis() { + CeTask task = newCeTaskBuilder() + .setStatus(CeTask.Status.SUCCESS) + .setId("#1") + .build(); + QualityGate gate = newQualityGateBuilder() + .setId("G1") + .setName("Gate One") + .setStatus(QualityGate.Status.WARN) + .add(newConditionBuilder() + .setMetricKey("coverage") + .setOperator(QualityGate.Operator.GREATER_THAN) + .setOnLeakPeriod(true) + .setWarningThreshold("75.0") + .setErrorThreshold("70.0") + .build(QualityGate.EvaluationStatus.WARN, "74.0")) + .build(); + PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate); + + WebhookPayload payload = WebhookPayload.from(analysis); + assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY); + assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/success.json")); + } + + @Test + public void create_payload_for_failed_analysis() { + CeTask ceTask = newCeTaskBuilder().setStatus(CeTask.Status.FAILED).setId("#1").build(); + PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(ceTask, null); + + WebhookPayload payload = WebhookPayload.from(analysis); + + assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY); + assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/failed.json")); + } + + @Test + public void create_payload_with_gate_conditions_without_value() { + CeTask task = newCeTaskBuilder() + .setStatus(CeTask.Status.SUCCESS) + .setId("#1") + .build(); + QualityGate gate = newQualityGateBuilder() + .setId("G1") + .setName("Gate One") + .setStatus(QualityGate.Status.WARN) + .add(newConditionBuilder() + .setMetricKey("coverage") + .setOperator(QualityGate.Operator.GREATER_THAN) + .setWarningThreshold("75.0") + .setErrorThreshold("70.0") + .buildNoValue()) + .build(); + PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate); + + WebhookPayload payload = WebhookPayload.from(analysis); + assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY); + assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/gate_condition_without_value.json")); + } + + private static PostProjectAnalysisTask.ProjectAnalysis newAnalysis(CeTask task, @Nullable QualityGate gate) { + return new PostProjectAnalysisTask.ProjectAnalysis() { + @Override + public CeTask getCeTask() { + return task; + } + + @Override + public Project getProject() { + return newProjectBuilder() + .setUuid("P1_UUID") + .setKey(PROJECT_KEY) + .setName("Project One") + .build(); + } + + @Override + public QualityGate getQualityGate() { + return gate; + } + + @Override + public Date getDate() { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getAnalysisDate() { + return Optional.of(new Date(1_500_000_000_000L)); + } + + @Override + public ScannerContext getScannerContext() { + return newScannerContextBuilder().build(); + } + }; + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java new file mode 100644 index 00000000000..ac8224ce0f6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.computation.task.projectanalysis.webhook; + +import java.io.IOException; +import java.util.Date; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.ce.posttask.CeTask; +import org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester; +import org.sonar.api.config.MapSettings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository; +import org.sonar.server.computation.task.projectanalysis.component.TestSettingsRepository; +import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder; +import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder; +import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.DUMB_PROJECT; + +public class WebhookPostTaskTest { + + private static final long NOW = 1_500_000_000_000L; + + @Rule + public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG); + + @Rule + public TreeRootHolderRule rootHolder = new TreeRootHolderRule().setRoot(DUMB_PROJECT); + + private final MapSettings settings = new MapSettings(); + private final TestWebhookCaller caller = new TestWebhookCaller(); + + @Test + public void do_nothing_if_no_webhooks() { + execute(); + + assertThat(caller.countSent()).isEqualTo(0); + assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty(); + } + + @Test + public void send_global_webhooks() { + settings.setProperty("sonar.webhooks.global", "1,2"); + settings.setProperty("sonar.webhooks.global.1.name", "First"); + settings.setProperty("sonar.webhooks.global.1.url", "http://url1"); + settings.setProperty("sonar.webhooks.global.2.name", "Second"); + settings.setProperty("sonar.webhooks.global.2.url", "http://url2"); + caller.enqueueSuccess(NOW, 200, 1_234); + caller.enqueueFailure(NOW, new IOException("Fail to connect")); + + execute(); + + assertThat(caller.countSent()).isEqualTo(2); + assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200"); + assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to send webhook 'Second' | url=http://url2 | message=Fail to connect"); + } + @Test + public void send_project_webhooks() { + settings.setProperty("sonar.webhooks.project", "1"); + settings.setProperty("sonar.webhooks.project.1.name", "First"); + settings.setProperty("sonar.webhooks.project.1.url", "http://url1"); + caller.enqueueSuccess(NOW, 200, 1_234); + + execute(); + + assertThat(caller.countSent()).isEqualTo(1); + assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200"); + } + + private void execute() { + SettingsRepository settingsRepository = new TestSettingsRepository(settings); + WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller); + + PostProjectAnalysisTaskTester.of(task) + .at(new Date()) + .withCeTask(newCeTaskBuilder() + .setStatus(CeTask.Status.SUCCESS) + .setId("#1") + .build()) + .withProject(newProjectBuilder() + .setUuid("P1_UUID") + .setKey("P1") + .setName("Project One") + .build()) + .execute(); + } +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json new file mode 100644 index 00000000000..358c1f055ae --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json @@ -0,0 +1,8 @@ +{ + "taskId": "#1", + "status": "FAILED", + "project": { + "key": "P1", + "name": "Project One" + } +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json new file mode 100644 index 00000000000..7cdcfc965df --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json @@ -0,0 +1,23 @@ +{ + "taskId": "#1", + "status": "SUCCESS", + "analysedAt": "2017-07-14T04:40:00+0200", + "project": { + "key": "P1", + "name": "Project One" + }, + "qualityGate": { + "name": "Gate One", + "status": "WARN", + "conditions": [ + { + "metric": "coverage", + "operator": "GREATER_THAN", + "status": "NO_VALUE", + "onLeakPeriod": false, + "errorThreshold": "70.0", + "warningThreshold": "75.0" + } + ] + } +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json new file mode 100644 index 00000000000..fdb7f8eb112 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json @@ -0,0 +1,24 @@ +{ + "taskId": "#1", + "status": "SUCCESS", + "analysedAt": "2017-07-14T04:40:00+0200", + "project": { + "key": "P1", + "name": "Project One" + }, + "qualityGate": { + "name": "Gate One", + "status": "WARN", + "conditions": [ + { + "metric": "coverage", + "operator": "GREATER_THAN", + "value": "74.0", + "status": "WARN", + "onLeakPeriod": true, + "errorThreshold": "70.0", + "warningThreshold": "75.0" + } + ] + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java index d4051edd6f3..82c74404660 100644 --- a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java +++ b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java @@ -62,6 +62,7 @@ public class CorePropertyDefinitions { defs.addAll(DebtProperties.all()); defs.addAll(PurgeProperties.all()); defs.addAll(EmailSettings.definitions()); + defs.addAll(WebhookProperties.all()); defs.addAll(ImmutableList.of( PropertyDefinition.builder(PROP_PASSWORD) diff --git a/sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java b/sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java new file mode 100644 index 00000000000..6c2fb504761 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.core.config; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.sonar.api.PropertyType; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyFieldDefinition; +import org.sonar.api.resources.Qualifiers; + +public class WebhookProperties { + + public static final String GLOBAL_KEY = "sonar.webhooks.global"; + public static final String PROJECT_KEY = "sonar.webhooks.project"; + public static final String NAME_FIELD = "name"; + public static final String URL_FIELD = "url"; + private static final String CATEGORY = "webhooks"; + private static final String DESCRIPTION = "Webhooks are used to notify external services when a project analysis is done. " + + "A HTTP POST request including a JSON payload is sent to each of the provided URLs. " + + "Learn more in the Webhooks documentation."; + + private WebhookProperties() { + // only static stuff + } + + static List all() { + return ImmutableList.of( + PropertyDefinition.builder(GLOBAL_KEY) + .category(CATEGORY) + .name("Webhooks") + .description(DESCRIPTION) + .fields( + PropertyFieldDefinition.build(NAME_FIELD) + .name("Name") + .type(PropertyType.STRING) + .build(), + PropertyFieldDefinition.build(URL_FIELD) + .name("URL") + .type(PropertyType.STRING) + .build()) + .build(), + + PropertyDefinition.builder(PROJECT_KEY) + .category(CATEGORY) + .name("Project Webhooks") + .description(DESCRIPTION) + .onlyOnQualifiers(Qualifiers.PROJECT) + .fields( + PropertyFieldDefinition.build(NAME_FIELD) + .name("Name") + .type(PropertyType.STRING) + .build(), + PropertyFieldDefinition.build(URL_FIELD) + .name("URL") + .type(PropertyType.STRING) + .build()) + .build()); + } +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 76ab0fc2ad1..f8ad062fc57 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -893,6 +893,7 @@ property.category.duplications=Duplications property.category.localization=Localization property.category.server_id=Server ID property.category.exclusions=Analysis Scope +property.category.webhooks=Webhooks property.sonar.inclusions.name=Source File Inclusions property.sonar.inclusions.description=Patterns used to include some source files and only these ones in analysis. property.sonar.test.inclusions.name=Test File Inclusions diff --git a/sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java b/sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java index 4e66c693efb..bf9d2501497 100644 --- a/sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java +++ b/sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java @@ -35,7 +35,7 @@ public class CorePropertyDefinitionsTest { @Test public void all() { List defs = CorePropertyDefinitions.all(); - assertThat(defs.size()).isGreaterThan(9); + assertThat(defs).hasSize(64); } @Test -- 2.39.5