+ 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();
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;
// views
ViewIndex.class,
- MeasureToMeasureDto.class);
+ MeasureToMeasureDto.class,
+
+ // webhooks
+ WebhookModule.class);
}
}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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.
+ * <br/>
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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<Integer> getHttpStatus() {
+ return Optional.ofNullable(httpStatus);
+ }
+
+ /**
+ * @return the duration in milliseconds if {@link #getThrowable()} is empty,
+ * else returns {@link Optional#empty()}
+ */
+ public Optional<Long> 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<Throwable> 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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<Date> 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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<String> webhookProps = Iterables.concat(
+ getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY),
+ getWebhookProperties(settings, WebhookProperties.PROJECT_KEY)
+ );
+ if (!Iterables.isEmpty(webhookProps)) {
+ process(settings, analysis, webhookProps);
+ }
+ }
+
+ private static List<String> 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<String> 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> 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));
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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<Item> 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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<Date> getAnalysisDate() {
+ return Optional.of(new Date(1_500_000_000_000L));
+ }
+
+ @Override
+ public ScannerContext getScannerContext() {
+ return newScannerContextBuilder().build();
+ }
+ };
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+{
+ "taskId": "#1",
+ "status": "FAILED",
+ "project": {
+ "key": "P1",
+ "name": "Project One"
+ }
+}
--- /dev/null
+{
+ "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"
+ }
+ ]
+ }
+}
--- /dev/null
+{
+ "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"
+ }
+ ]
+ }
+}
defs.addAll(DebtProperties.all());
defs.addAll(PurgeProperties.all());
defs.addAll(EmailSettings.definitions());
+ defs.addAll(WebhookProperties.all());
defs.addAll(ImmutableList.of(
PropertyDefinition.builder(PROP_PASSWORD)
--- /dev/null
+/*
+ * 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 <a href=\"#\">Webhooks documentation</a>.";
+
+ private WebhookProperties() {
+ // only static stuff
+ }
+
+ static List<PropertyDefinition> 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());
+ }
+}
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
@Test
public void all() {
List<PropertyDefinition> defs = CorePropertyDefinitions.all();
- assertThat(defs.size()).isGreaterThan(9);
+ assertThat(defs).hasSize(64);
}
@Test