@@ -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(); |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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. | |||
* <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); | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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<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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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<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(); | |||
} | |||
} | |||
} |
@@ -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<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)); | |||
} | |||
} | |||
} |
@@ -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; |
@@ -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; | |||
} | |||
} |
@@ -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<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; | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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<Date> getAnalysisDate() { | |||
return Optional.of(new Date(1_500_000_000_000L)); | |||
} | |||
@Override | |||
public ScannerContext getScannerContext() { | |||
return newScannerContextBuilder().build(); | |||
} | |||
}; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
{ | |||
"taskId": "#1", | |||
"status": "FAILED", | |||
"project": { | |||
"key": "P1", | |||
"name": "Project One" | |||
} | |||
} |
@@ -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" | |||
} | |||
] | |||
} | |||
} |
@@ -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" | |||
} | |||
] | |||
} | |||
} |
@@ -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) |
@@ -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 <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()); | |||
} | |||
} |
@@ -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 |
@@ -35,7 +35,7 @@ public class CorePropertyDefinitionsTest { | |||
@Test | |||
public void all() { | |||
List<PropertyDefinition> defs = CorePropertyDefinitions.all(); | |||
assertThat(defs.size()).isGreaterThan(9); | |||
assertThat(defs).hasSize(64); | |||
} | |||
@Test |