Watcher: Add proxy support for reporting attachment action ()

This allows to configure a proxy for the reporting attachment
action. The proxy is used by the HTTP client.

Original commit: elastic/x-pack-elasticsearch@87b6ab1b68
This commit is contained in:
Alexander Reelsen 2017-06-20 13:49:32 +02:00 committed by GitHub
parent 6e6629bd18
commit d6254c9fd3
4 changed files with 112 additions and 53 deletions
docs/en/watcher/actions
plugin/src
main/java/org/elasticsearch/xpack/notification/email/attachment
test/java/org/elasticsearch/xpack/notification/email/attachment

@ -130,20 +130,21 @@ killed by firewalls or load balancers inbetween.
.`reporting` attachment type attributes
[options="header"]
|=====
| Name | Description
| `url` | The URL to trigger the dashboard creation
| `inline` | Configures as an attachment to sent with disposition `inline`. This
allows the use of embedded images in HTML bodies, which are displayed
in certain email clients. Optional. Defaults to `false`.
| `retries` | The reporting attachment type tries to poll regularly to receive the
created PDF. This configures the number of retries. Defaults to `40`.
The setting `xpack.notification.reporting.retries` can be configured
globally to change the default.
| `interval` | The time to wait between two polling tries. Defaults to `15s` (this
means, by default watcher tries to download a dashboard for 10 minutes,
forty times fifteen seconds). The setting `xpack.notification.reporting.interval`
can be configured globally to change the default.
| `request.auth` | Additional auth information for the request
| Name | Description
| `url` | The URL to trigger the dashboard creation
| `inline` | Configures as an attachment to sent with disposition `inline`. This
allows the use of embedded images in HTML bodies, which are displayed
in certain email clients. Optional. Defaults to `false`.
| `retries` | The reporting attachment type tries to poll regularly to receive the
created PDF. This configures the number of retries. Defaults to `40`.
The setting `xpack.notification.reporting.retries` can be configured
globally to change the default.
| `interval` | The time to wait between two polling tries. Defaults to `15s` (this
means, by default watcher tries to download a dashboard for 10 minutes,
forty times fifteen seconds). The setting `xpack.notification.reporting.interval`
can be configured globally to change the default.
| `request.auth` | Additional auth configuration for the request
| `request.proxy` | Additional proxy configuration for the request
|======

@ -9,6 +9,7 @@ import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.common.http.HttpProxy;
import org.elasticsearch.xpack.common.http.auth.HttpAuth;
import java.io.IOException;
@ -16,11 +17,12 @@ import java.util.Objects;
public class ReportingAttachment implements EmailAttachmentParser.EmailAttachment {
private static final ParseField INLINE = new ParseField("inline");
private static final ParseField AUTH = new ParseField("auth");
private static final ParseField INTERVAL = new ParseField("interval");
private static final ParseField RETRIES = new ParseField("retries");
private static final ParseField URL = new ParseField("url");
static final ParseField INLINE = new ParseField("inline");
static final ParseField AUTH = new ParseField("auth");
static final ParseField PROXY = new ParseField("proxy");
static final ParseField INTERVAL = new ParseField("interval");
static final ParseField RETRIES = new ParseField("retries");
static final ParseField URL = new ParseField("url");
private final boolean inline;
private final String id;
@ -28,19 +30,17 @@ public class ReportingAttachment implements EmailAttachmentParser.EmailAttachmen
private final String url;
private final TimeValue interval;
private final Integer retries;
private final HttpProxy proxy;
public ReportingAttachment(String id, String url, boolean inline) {
this(id, url, inline, null, null, null);
}
public ReportingAttachment(String id, String url, boolean inline, @Nullable TimeValue interval, @Nullable Integer retries,
@Nullable HttpAuth auth) {
ReportingAttachment(String id, String url, boolean inline, @Nullable TimeValue interval, @Nullable Integer retries,
@Nullable HttpAuth auth, @Nullable HttpProxy proxy) {
this.id = id;
this.url = url;
this.retries = retries;
this.inline = inline;
this.auth = auth;
this.interval = interval;
this.proxy = proxy;
if (retries != null && retries < 0) {
throw new IllegalArgumentException("Retries for attachment must be >= 0");
}
@ -77,6 +77,10 @@ public class ReportingAttachment implements EmailAttachmentParser.EmailAttachmen
return retries;
}
public HttpProxy proxy() {
return proxy;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(id).startObject(ReportingAttachmentParser.TYPE)
@ -100,6 +104,10 @@ public class ReportingAttachment implements EmailAttachmentParser.EmailAttachmen
builder.endObject();
}
if (proxy != null) {
proxy.toXContent(builder, params);
}
return builder.endObject().endObject();
}
@ -111,11 +119,12 @@ public class ReportingAttachment implements EmailAttachmentParser.EmailAttachmen
ReportingAttachment otherAttachment = (ReportingAttachment) o;
return Objects.equals(id, otherAttachment.id) && Objects.equals(url, otherAttachment.url) &&
Objects.equals(interval, otherAttachment.interval) && Objects.equals(inline, otherAttachment.inline) &&
Objects.equals(retries, otherAttachment.retries) && Objects.equals(auth, otherAttachment.auth);
Objects.equals(retries, otherAttachment.retries) && Objects.equals(auth, otherAttachment.auth) &&
Objects.equals(proxy, otherAttachment.proxy);
}
@Override
public int hashCode() {
return Objects.hash(id, url, interval, inline, retries, auth);
return Objects.hash(id, url, interval, inline, retries, auth, proxy);
}
}

@ -21,6 +21,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.common.http.HttpClient;
import org.elasticsearch.xpack.common.http.HttpMethod;
import org.elasticsearch.xpack.common.http.HttpProxy;
import org.elasticsearch.xpack.common.http.HttpRequest;
import org.elasticsearch.xpack.common.http.HttpRequestTemplate;
import org.elasticsearch.xpack.common.http.HttpResponse;
@ -52,11 +53,12 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
new ObjectParser<>("reporting_attachment_kibana_payload", true, null);
static {
PARSER.declareInt(Builder::retries, new ParseField("retries"));
PARSER.declareBoolean(Builder::inline, new ParseField("inline"));
PARSER.declareString(Builder::interval, new ParseField("interval"));
PARSER.declareString(Builder::url, new ParseField("url"));
PARSER.declareObjectOrDefault(Builder::auth, (p, s) -> s.parseAuth(p), () -> null, new ParseField("auth"));
PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES);
PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE);
PARSER.declareString(Builder::interval, ReportingAttachment.INTERVAL);
PARSER.declareString(Builder::url, ReportingAttachment.URL);
PARSER.declareObjectOrDefault(Builder::auth, (p, s) -> s.parseAuth(p), () -> null, ReportingAttachment.AUTH);
PARSER.declareObjectOrDefault(Builder::proxy, (p, s) -> s.parseProxy(p), () -> null, ReportingAttachment.PROXY);
PAYLOAD_PARSER.declareString(KibanaReportingPayload::setPath, new ParseField("path"));
}
@ -100,6 +102,7 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
.readTimeout(TimeValue.timeValueSeconds(15))
.method(HttpMethod.POST)
.auth(attachment.auth())
.proxy(attachment.proxy())
.putHeader("kbn-xsrf", new TextTemplate("reporting"))
.build();
HttpRequest request = requestTemplate.render(templateEngine, model);
@ -113,6 +116,7 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
.auth(attachment.auth())
.path(path)
.scheme(request.scheme())
.proxy(attachment.proxy())
.putHeader("kbn-xsrf", new TextTemplate("reporting"))
.build();
HttpRequest pollingRequest = pollingRequestTemplate.render(templateEngine, model);
@ -207,7 +211,7 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
}
/**
* A helper class to parse the HTTPAuth data, which is read by an old school pull parser, that is handed over in the ctor.
* A helper class to parse HTTP auth and proxy structures, which is read by an old school pull parser, that is handed over in the ctor.
* See the static parser definition at the top
*/
private static class AuthParseContext {
@ -225,6 +229,14 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
throw new UncheckedIOException(e);
}
}
HttpProxy parseProxy(XContentParser parser) {
try {
return HttpProxy.parse(parser);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
/**
@ -256,6 +268,7 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
private TimeValue interval;
private Integer retries;
private HttpAuth auth;
private HttpProxy proxy;
Builder(String id) {
this.id = id;
@ -287,8 +300,13 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
return this;
}
Builder proxy(HttpProxy proxy) {
this.proxy = proxy;
return this;
}
ReportingAttachment build() {
return new ReportingAttachment(id, url, inline, interval, retries, auth);
return new ReportingAttachment(id, url, inline, interval, retries, auth, proxy);
}
}
}

@ -16,6 +16,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.http.HttpClient;
import org.elasticsearch.xpack.common.http.HttpMethod;
import org.elasticsearch.xpack.common.http.HttpProxy;
import org.elasticsearch.xpack.common.http.HttpRequest;
import org.elasticsearch.xpack.common.http.HttpResponse;
import org.elasticsearch.xpack.common.http.auth.HttpAuth;
@ -120,6 +121,16 @@ public class ReportingAttachmentParserTests extends ESTestCase {
auth = new BasicAuth("foo", "secret".toCharArray());
}
HttpProxy proxy = null;
boolean withProxy = randomBoolean();
if (withProxy) {
proxy = new HttpProxy("example.org", 8080);
builder.startObject("proxy")
.field("host", proxy.getHost())
.field("port", proxy.getPort())
.endObject();
}
builder.endObject().endObject().endObject();
XContentParser parser = createParser(builder);
@ -133,7 +144,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
assertThat(toXcontentBuilder.string(), is(builder.string()));
XContentBuilder attachmentXContentBuilder = jsonBuilder().startObject();
ReportingAttachment attachment = new ReportingAttachment(id, dashboardUrl, isInline, interval, retries, auth);
ReportingAttachment attachment = new ReportingAttachment(id, dashboardUrl, isInline, interval, retries, auth, proxy);
attachment.toXContent(attachmentXContentBuilder, ToXContent.EMPTY_PARAMS);
attachmentXContentBuilder.endObject();
assertThat(attachmentXContentBuilder.string(), is(builder.string()));
@ -158,7 +169,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(200, content, headers));
ReportingAttachment reportingAttachment =
new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null);
new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class));
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
@ -178,15 +189,13 @@ public class ReportingAttachmentParserTests extends ESTestCase {
}
// test that the header "kbn-xsrf" has been set to "reporting" in all requests
requestArgumentCaptor.getAllValues().stream().forEach((req) -> {
assertThat(req.headers(), hasEntry("kbn-xsrf", "reporting"));
});
requestArgumentCaptor.getAllValues().forEach((req) -> assertThat(req.headers(), hasEntry("kbn-xsrf", "reporting")));
}
public void testInitialRequestFailsWithError() throws Exception {
when(httpClient.execute(any(HttpRequest.class)))
.thenReturn(new HttpResponse(403));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null);
ElasticsearchException e = expectThrows(ElasticsearchException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -195,7 +204,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
public void testInitialRequestThrowsIOException() throws Exception {
when(httpClient.execute(any(HttpRequest.class))).thenThrow(new IOException("Connection timed out"));
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/", randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), null, null, null, null);
IOException e = expectThrows(IOException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
assertThat(e.getMessage(), containsString("Connection timed out"));
@ -205,7 +214,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
when(httpClient.execute(any(HttpRequest.class)))
// closing json bracket is missing
.thenReturn(new HttpResponse(200, "{\"path\":\"anything\""));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null);
JsonEOFException e = expectThrows(JsonEOFException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
assertThat(e.getMessage(), containsString("Unexpected end-of-input"));
@ -215,7 +224,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
when(httpClient.execute(any(HttpRequest.class)))
// closing json bracket is missing
.thenReturn(new HttpResponse(200, "{\"path\": { \"foo\" : \"anything\"}}"));
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/", randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), null, null, null, null);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
assertThat(e.getMessage(),
@ -224,7 +233,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
public void testInitialRequestDoesNotContainPathInJson() throws Exception {
when(httpClient.execute(any(HttpRequest.class))).thenReturn(new HttpResponse(200, "{\"foo\":\"bar\"}"));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null);
ElasticsearchException e = expectThrows(ElasticsearchException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
assertThat(e.getMessage(), containsString("Watch[watch1] reporting[foo] field path found in JSON payload"));
@ -236,7 +245,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(403));
ReportingAttachment attachment =
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), 10, null);
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
ElasticsearchException e = expectThrows(ElasticsearchException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -250,7 +259,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(503));
ReportingAttachment attachment =
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), 1, null);
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), 1, null, null);
ElasticsearchException e = expectThrows(ElasticsearchException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -263,7 +272,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(1));
ReportingAttachment attachment =
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), null, null);
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), null, null, null);
IllegalStateException e = expectThrows(IllegalStateException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -276,7 +285,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenThrow(new IOException("whatever"));
ReportingAttachment attachment =
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), null, null);
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), null, null, null);
IOException e = expectThrows(IOException.class,
() -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -291,7 +300,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(200, content));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(),
TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()));
TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()), null);
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment);
@ -314,7 +323,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(503));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1),
ReportingAttachmentParser.RETRIES_SETTING.getDefault(Settings.EMPTY), new BasicAuth("foo", "bar".toCharArray()));
ReportingAttachmentParser.RETRIES_SETTING.getDefault(Settings.EMPTY), new BasicAuth("foo", "bar".toCharArray()), null);
expectThrows(ElasticsearchException.class, () ->
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -327,7 +336,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(200, "{\"path\":\"whatever\"}"))
.thenReturn(new HttpResponse(503));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean());
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null);
Settings settings = Settings.builder()
.put(ReportingAttachmentParser.INTERVAL_SETTING.getKey(), "1ms")
@ -355,7 +364,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
};
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(),
TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()));
TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()), null);
reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient,
replaceHttpWithHttpsTemplateEngine, authRegistry);
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment);
@ -369,7 +378,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
public void testRetrySettingCannotBeNegative() throws Exception {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(), null, -10, null));
new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(), null, -10, null, null));
assertThat(e.getMessage(), is("Retries for attachment must be >= 0"));
Settings invalidSettings = Settings.builder().put("xpack.notification.reporting.retries", -10).build();
@ -378,6 +387,28 @@ public class ReportingAttachmentParserTests extends ESTestCase {
assertThat(e.getMessage(), is("Failed to parse value [-10] for setting [xpack.notification.reporting.retries] must be >= 0"));
}
public void testHttpProxy() throws Exception {
String content = randomAlphaOfLength(200);
String path = "/ovb/api/reporting/jobs/download/iu5zfzvk15oa8990bfas9wy2";
String randomContentType = randomAlphaOfLength(20);
Map<String, String[]> headers = new HashMap<>();
headers.put("Content-Type", new String[] { randomContentType });
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.execute(requestCaptor.capture()))
.thenReturn(new HttpResponse(200, "{\"path\":\""+ path +"\", \"other\":\"content\"}"))
.thenReturn(new HttpResponse(503))
.thenReturn(new HttpResponse(200, content, headers));
HttpProxy proxy = new HttpProxy("localhost", 8080);
ReportingAttachment reportingAttachment =
new ReportingAttachment("foo", "http://www.example.org/", randomBoolean(), TimeValue.timeValueMillis(1), null, null, proxy);
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(requestCaptor.getAllValues(), hasSize(3));
requestCaptor.getAllValues().forEach(req -> assertThat(req.proxy(), is(proxy)));
}
private WatchExecutionContext createWatchExecutionContext() {
DateTime now = DateTime.now(DateTimeZone.UTC);
return mockExecutionContextBuilder("watch1")