Explorar el Código

SONAR-8351 Send JSON payload over HTTP when project analysis is complete

tags/6.2-RC1
Simon Brandhof hace 7 años
padre
commit
8301fe8e28
Se han modificado 22 ficheros con 1132 adiciones y 3 borrados
  1. 1
    1
      server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
  2. 5
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
  3. 44
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java
  4. 34
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java
  5. 69
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java
  6. 131
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java
  7. 31
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java
  8. 107
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java
  9. 96
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
  10. 23
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java
  11. 40
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java
  12. 75
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java
  13. 92
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java
  14. 142
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java
  15. 107
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
  16. 8
    0
      server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json
  17. 23
    0
      server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json
  18. 24
    0
      server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json
  19. 1
    0
      sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
  20. 77
    0
      sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java
  21. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  22. 1
    1
      sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java

+ 1
- 1
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java Ver fichero

@@ -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();

+ 5
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java Ver fichero

@@ -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);
}

}

+ 44
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java Ver fichero

@@ -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;
}
}

+ 34
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java Ver fichero

@@ -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);

}

+ 69
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java Ver fichero

@@ -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();
}

}

+ 131
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java Ver fichero

@@ -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);
}
}
}

+ 31
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java Ver fichero

@@ -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);
}
}

+ 107
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java Ver fichero

@@ -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();
}
}
}

+ 96
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java Ver fichero

@@ -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));
}
}
}

+ 23
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java Ver fichero

@@ -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;

+ 40
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java Ver fichero

@@ -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;
}
}

+ 75
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java Ver fichero

@@ -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;
}
}
}

+ 92
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java Ver fichero

@@ -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));
}
}

+ 142
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java Ver fichero

@@ -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();
}
};
}

}

+ 107
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java Ver fichero

@@ -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();
}
}

+ 8
- 0
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json Ver fichero

@@ -0,0 +1,8 @@
{
"taskId": "#1",
"status": "FAILED",
"project": {
"key": "P1",
"name": "Project One"
}
}

+ 23
- 0
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json Ver fichero

@@ -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"
}
]
}
}

+ 24
- 0
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json Ver fichero

@@ -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"
}
]
}
}

+ 1
- 0
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java Ver fichero

@@ -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)

+ 77
- 0
sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java Ver fichero

@@ -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());
}
}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver fichero

@@ -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

+ 1
- 1
sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java Ver fichero

@@ -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

Cargando…
Cancelar
Guardar