Watcher: Add JIRA action (elastic/elasticsearch#4014)

closes elastic/elasticsearch#493

Original commit: elastic/x-pack-elasticsearch@6b7387d3e4
This commit is contained in:
Tanguy Leroux 2016-11-21 10:52:55 +01:00 committed by GitHub
parent 74b0a1e71a
commit 18478d63c2
20 changed files with 1937 additions and 14 deletions

View File

@ -1,9 +1,10 @@
import org.elasticsearch.gradle.MavenFilteringHack
import org.elasticsearch.gradle.test.NodeInfo
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import org.elasticsearch.gradle.MavenFilteringHack
import org.elasticsearch.gradle.test.NodeInfo
group 'org.elasticsearch.plugin'
@ -247,3 +248,4 @@ thirdPartyAudit.excludes = [
'javax.activation.URLDataSource',
'javax.activation.UnsupportedDataTypeException'
]

View File

@ -71,6 +71,7 @@ import org.elasticsearch.xpack.notification.email.attachment.HttpEmailAttachemen
import org.elasticsearch.xpack.notification.email.attachment.ReportingAttachmentParser;
import org.elasticsearch.xpack.notification.email.support.BodyPartSource;
import org.elasticsearch.xpack.notification.hipchat.HipChatService;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.notification.pagerduty.PagerDutyAccount;
import org.elasticsearch.xpack.notification.pagerduty.PagerDutyService;
import org.elasticsearch.xpack.notification.slack.SlackService;
@ -261,6 +262,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I
List<Object> components = new ArrayList<>();
components.add(new EmailService(settings, security.getCryptoService(), clusterSettings));
components.add(new HipChatService(settings, httpClient, clusterSettings));
components.add(new JiraService(settings, httpClient, clusterSettings));
components.add(new SlackService(settings, httpClient, clusterSettings));
components.add(new PagerDutyService(settings, httpClient, clusterSettings));
@ -320,6 +322,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I
settings.add(SlackService.SLACK_ACCOUNT_SETTING);
settings.add(EmailService.EMAIL_ACCOUNT_SETTING);
settings.add(HipChatService.HIPCHAT_ACCOUNT_SETTING);
settings.add(JiraService.JIRA_ACCOUNT_SETTING);
settings.add(PagerDutyService.PAGERDUTY_ACCOUNT_SETTING);
settings.add(ReportingAttachmentParser.RETRIES_SETTING);
settings.add(ReportingAttachmentParser.INTERVAL_SETTING);
@ -336,6 +339,7 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin, I
public List<String> getSettingsFilter() {
List<String> filters = new ArrayList<>();
filters.add("xpack.notification.email.account.*.smtp.password");
filters.add("xpack.notification.jira.account.*.password");
filters.add("xpack.notification.slack.account.*.url");
filters.add("xpack.notification.pagerduty.account.*.url");
filters.add("xpack.notification.pagerduty." + PagerDutyAccount.SERVICE_KEY_SETTING);

View File

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.notification.jira;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
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.Scheme;
import org.elasticsearch.xpack.common.http.auth.basic.BasicAuth;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Map;
public class JiraAccount {
/**
* Default JIRA REST API path for create issues
**/
public static final String DEFAULT_PATH = "/rest/api/2/issue";
static final String USER_SETTING = "user";
static final String PASSWORD_SETTING = "password";
static final String URL_SETTING = "url";
static final String ISSUE_DEFAULTS_SETTING = "issue_defaults";
static final String ALLOW_HTTP_SETTING = "allow_http";
private final HttpClient httpClient;
private final String name;
private final String user;
private final String password;
private final URI url;
private final Map<String, Object> issueDefaults;
public JiraAccount(String name, Settings settings, HttpClient httpClient) {
this.httpClient = httpClient;
this.name = name;
String url = settings.get(URL_SETTING);
if (url == null) {
throw requiredSettingException(name, URL_SETTING);
}
try {
URI uri = new URI(url);
Scheme protocol = Scheme.parse(uri.getScheme());
if ((protocol == Scheme.HTTP) && (Booleans.isExplicitTrue(settings.get(ALLOW_HTTP_SETTING)) == false)) {
throw new SettingsException("invalid jira [" + name + "] account settings. unsecure scheme [" + protocol + "]");
}
this.url = uri;
} catch (URISyntaxException | IllegalArgumentException e) {
throw new SettingsException("invalid jira [" + name + "] account settings. invalid [" + URL_SETTING + "] setting", e);
}
this.user = settings.get(USER_SETTING);
if (Strings.isEmpty(this.user)) {
throw requiredSettingException(name, USER_SETTING);
}
this.password = settings.get(PASSWORD_SETTING);
if (Strings.isEmpty(this.password)) {
throw requiredSettingException(name, PASSWORD_SETTING);
}
this.issueDefaults = Collections.unmodifiableMap(settings.getAsSettings(ISSUE_DEFAULTS_SETTING).getAsStructuredMap());
}
public String getName() {
return name;
}
public Map<String, Object> getDefaults() {
return issueDefaults;
}
public JiraIssue createIssue(final Map<String, Object> fields, final HttpProxy proxy) throws IOException {
HttpRequest request = HttpRequest.builder(url.getHost(), url.getPort())
.scheme(Scheme.parse(url.getScheme()))
.method(HttpMethod.POST)
.path(DEFAULT_PATH)
.jsonBody((builder, params) -> builder.field("fields", fields))
.auth(new BasicAuth(user, password.toCharArray()))
.proxy(proxy)
.build();
HttpResponse response = httpClient.execute(request);
return JiraIssue.responded(fields, request, response);
}
private static SettingsException requiredSettingException(String account, String setting) {
return new SettingsException("invalid jira [" + account + "] account settings. missing required [" + setting + "] setting");
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.notification.jira;
import org.apache.http.HttpStatus;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.common.http.HttpRequest;
import org.elasticsearch.xpack.common.http.HttpResponse;
import org.elasticsearch.xpack.watcher.actions.jira.JiraAction;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class JiraIssue implements ToXContent {
private final Map<String, Object> fields;
@Nullable private final HttpRequest request;
@Nullable private final HttpResponse response;
@Nullable private final String failureReason;
public static JiraIssue responded(Map<String, Object> fields, HttpRequest request, HttpResponse response) {
return new JiraIssue(fields, request, response, resolveFailureReason(response));
}
JiraIssue(Map<String, Object> fields, HttpRequest request, HttpResponse response, String failureReason) {
this.fields = fields;
this.request = request;
this.response = response;
this.failureReason = failureReason;
}
public boolean successful() {
return failureReason == null;
}
public HttpRequest getRequest() {
return request;
}
public HttpResponse getResponse() {
return response;
}
public Map<String, Object> getFields() {
return fields;
}
public String getFailureReason() {
return failureReason;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JiraIssue sentEvent = (JiraIssue) o;
return Objects.equals(fields, sentEvent.fields) &&
Objects.equals(request, sentEvent.request) &&
Objects.equals(response, sentEvent.response) &&
Objects.equals(failureReason, sentEvent.failureReason);
}
@Override
public int hashCode() {
return Objects.hash(fields, request, response, failureReason);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (fields != null) {
builder.field(Field.FIELDS.getPreferredName(), fields);
}
if (successful() == false) {
builder.field(Field.REASON.getPreferredName(), failureReason);
if (request != null) {
builder.field(Field.REQUEST.getPreferredName(), request, params);
}
if (response != null) {
builder.field(Field.RESPONSE.getPreferredName(), response, params);
}
} else {
builder.rawField(Field.RESULT.getPreferredName(), response.body());
}
return builder.endObject();
}
/**
* Resolve the failure reason, when a reason can be extracted from the response body:
* Ex: {"errorMessages":[],"errors":{"customfield_10004":"Epic Name is required."}}
* <p>
* See https://docs.atlassian.com/jira/REST/cloud/ for the format of the error response body.
*/
static String resolveFailureReason(HttpResponse response) {
int status = response.status();
if (status < 300) {
return null;
}
StringBuilder message = new StringBuilder();
switch (status) {
case HttpStatus.SC_BAD_REQUEST:
message.append("Bad Request");
break;
case HttpStatus.SC_UNAUTHORIZED:
message.append("Unauthorized (authentication credentials are invalid)");
break;
case HttpStatus.SC_FORBIDDEN:
message.append("Forbidden (account doesn't have permission to create this issue)");
break;
case HttpStatus.SC_NOT_FOUND:
message.append("Not Found (account uses invalid JIRA REST APIs)");
break;
case HttpStatus.SC_REQUEST_TIMEOUT:
message.append("Request Timeout (request took too long to process)");
break;
case HttpStatus.SC_INTERNAL_SERVER_ERROR:
message.append("JIRA Server Error (internal error occurred while processing request)");
break;
default:
message.append("Unknown Error");
break;
}
if (response.hasContent()) {
final List<String> errors = new ArrayList<>();
try (XContentParser parser = JsonXContent.jsonXContent.createParser(response.body())) {
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
}
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse jira project. expected an object, but found [{}] instead",
token);
}
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.ERRORS)) {
Map<String, Object> fieldErrors = parser.mapOrdered();
for (Map.Entry<String, Object> entry : fieldErrors.entrySet()) {
errors.add("Field [" + entry.getKey() + "] has error [" + String.valueOf(entry.getValue()) + "]");
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.ERROR_MESSAGES)) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
errors.add(parser.text());
}
} else {
throw new ElasticsearchParseException("could not parse jira response. unexpected field [{}]", currentFieldName);
}
}
} catch (Exception e) {
errors.add("Exception when parsing jira response [" + String.valueOf(e) + "]");
}
if (errors.isEmpty() == false) {
message.append(" - ");
for (String error : errors) {
message.append(error).append('\n');
}
}
}
return message.toString();
}
private interface Field {
ParseField FIELDS = JiraAction.Field.FIELDS;
ParseField REASON = new ParseField("reason");
ParseField REQUEST = new ParseField("request");
ParseField RESPONSE = new ParseField("response");
ParseField RESULT = new ParseField("result");
ParseField ERROR_MESSAGES = new ParseField("errorMessages");
ParseField ERRORS = new ParseField("errors");
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.notification.jira;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.common.http.HttpClient;
import org.elasticsearch.xpack.notification.NotificationService;
/**
* A component to store Atlassian's JIRA credentials.
*
* https://www.atlassian.com/software/jira
*/
public class JiraService extends NotificationService<JiraAccount> {
public static final Setting<Settings> JIRA_ACCOUNT_SETTING =
Setting.groupSetting("xpack.notification.jira.", Setting.Property.Dynamic, Setting.Property.NodeScope);
private final HttpClient httpClient;
public JiraService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) {
super(settings);
this.httpClient = httpClient;
clusterSettings.addSettingsUpdateConsumer(JIRA_ACCOUNT_SETTING, this::setAccountSetting);
setAccountSetting(JIRA_ACCOUNT_SETTING.get(settings));
}
@Override
protected JiraAccount createAccount(String name, Settings accountSettings) {
return new JiraAccount(name, accountSettings, httpClient);
}
}

View File

@ -41,6 +41,7 @@ import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.email.EmailService;
import org.elasticsearch.xpack.notification.email.attachment.EmailAttachmentsParser;
import org.elasticsearch.xpack.notification.hipchat.HipChatService;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.notification.pagerduty.PagerDutyService;
import org.elasticsearch.xpack.notification.slack.SlackService;
import org.elasticsearch.xpack.security.InternalClient;
@ -53,6 +54,8 @@ import org.elasticsearch.xpack.watcher.actions.hipchat.HipChatAction;
import org.elasticsearch.xpack.watcher.actions.hipchat.HipChatActionFactory;
import org.elasticsearch.xpack.watcher.actions.index.IndexAction;
import org.elasticsearch.xpack.watcher.actions.index.IndexActionFactory;
import org.elasticsearch.xpack.watcher.actions.jira.JiraAction;
import org.elasticsearch.xpack.watcher.actions.jira.JiraActionFactory;
import org.elasticsearch.xpack.watcher.actions.logging.LoggingAction;
import org.elasticsearch.xpack.watcher.actions.logging.LoggingActionFactory;
import org.elasticsearch.xpack.watcher.actions.pagerduty.PagerDutyAction;
@ -238,6 +241,8 @@ public class Watcher implements ActionPlugin, ScriptPlugin {
actionFactoryMap.put(LoggingAction.TYPE, new LoggingActionFactory(settings, templateEngine));
actionFactoryMap.put(HipChatAction.TYPE, new HipChatActionFactory(settings, templateEngine,
getService(HipChatService.class, components)));
actionFactoryMap.put(JiraAction.TYPE, new JiraActionFactory(settings, templateEngine,
getService(JiraService.class, components)));
actionFactoryMap.put(SlackAction.TYPE, new SlackActionFactory(settings, templateEngine,
getService(SlackService.class, components)));
actionFactoryMap.put(PagerDutyAction.TYPE, new PagerDutyActionFactory(settings, templateEngine,

View File

@ -5,18 +5,22 @@
*/
package org.elasticsearch.xpack.watcher.actions;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.xpack.common.http.HttpRequestTemplate;
import org.elasticsearch.xpack.common.text.TextTemplate;
import org.elasticsearch.xpack.notification.email.EmailTemplate;
import org.elasticsearch.xpack.notification.pagerduty.IncidentEvent;
import org.elasticsearch.xpack.notification.slack.message.SlackMessage;
import org.elasticsearch.xpack.watcher.actions.email.EmailAction;
import org.elasticsearch.xpack.watcher.actions.hipchat.HipChatAction;
import org.elasticsearch.xpack.watcher.actions.index.IndexAction;
import org.elasticsearch.xpack.watcher.actions.jira.JiraAction;
import org.elasticsearch.xpack.watcher.actions.logging.LoggingAction;
import org.elasticsearch.xpack.watcher.actions.pagerduty.PagerDutyAction;
import org.elasticsearch.xpack.notification.email.EmailTemplate;
import org.elasticsearch.xpack.notification.pagerduty.IncidentEvent;
import org.elasticsearch.xpack.watcher.actions.slack.SlackAction;
import org.elasticsearch.xpack.notification.slack.message.SlackMessage;
import org.elasticsearch.xpack.watcher.actions.webhook.WebhookAction;
import org.elasticsearch.xpack.common.http.HttpRequestTemplate;
import org.elasticsearch.xpack.common.text.TextTemplate;
import java.util.Map;
public final class ActionBuilders {
@ -35,6 +39,14 @@ public final class ActionBuilders {
return IndexAction.builder(index, type);
}
public static JiraAction.Builder jiraAction(String account, MapBuilder<String, Object> fields) {
return jiraAction(account, fields.immutableMap());
}
public static JiraAction.Builder jiraAction(String account, Map<String, Object> fields) {
return JiraAction.builder(account, fields);
}
public static WebhookAction.Builder webhookAction(HttpRequestTemplate.Builder httpRequest) {
return webhookAction(httpRequest.build());
}
@ -59,7 +71,6 @@ public final class ActionBuilders {
return hipchatAction(account, new TextTemplate(body));
}
public static HipChatAction.Builder hipchatAction(TextTemplate body) {
return hipchatAction(null, body);
}

View File

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.xpack.common.text.TextTemplate;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.jira.JiraAccount;
import org.elasticsearch.xpack.notification.jira.JiraIssue;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.watcher.actions.Action;
import org.elasticsearch.xpack.watcher.actions.ExecutableAction;
import org.elasticsearch.xpack.watcher.execution.WatchExecutionContext;
import org.elasticsearch.xpack.watcher.support.Variables;
import org.elasticsearch.xpack.watcher.watch.Payload;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ExecutableJiraAction extends ExecutableAction<JiraAction> {
private final TextTemplateEngine engine;
private final JiraService jiraService;
public ExecutableJiraAction(JiraAction action, Logger logger, JiraService jiraService, TextTemplateEngine templateEngine) {
super(action, logger);
this.jiraService = jiraService;
this.engine = templateEngine;
}
@Override
public Action.Result execute(final String actionId, WatchExecutionContext ctx, Payload payload) throws Exception {
JiraAccount account = jiraService.getAccount(action.account);
if (account == null) {
// the account associated with this action was deleted
throw new IllegalStateException("account [" + action.account + "] was not found. perhaps it was deleted");
}
final Function<String, String> render = s -> engine.render(new TextTemplate(s), Variables.createCtxModel(ctx, payload));
Map<String, Object> fields = new HashMap<>();
// Apply action fields
fields = merge(fields, action.fields, render);
// Apply default fields
fields = merge(fields, account.getDefaults(), render);
if (ctx.simulateAction(actionId)) {
return new JiraAction.Simulated(fields);
}
JiraIssue result = account.createIssue(fields, action.proxy);
return JiraAction.executedResult(result);
}
/**
* Merges the defaults provided as the second parameter into the content of the first
* while applying a {@link Function} on both map key and map value.
*/
static Map<String, Object> merge(final Map<String, Object> fields, final Map<String, ?> defaults, final Function<String, String> fn) {
if (defaults != null) {
for (Map.Entry<String, ?> defaultEntry : defaults.entrySet()) {
Object value = defaultEntry.getValue();
if (value instanceof String) {
// Apply the transformation to a simple string
value = fn.apply((String) value);
} else if (value instanceof Map) {
// Apply the transformation to a map
value = merge(new HashMap<>(), (Map<String, ?>) value, fn);
} else if (value instanceof String[]) {
// Apply the transformation to an array of strings
String[] newValues = new String[((String[]) value).length];
for (int i = 0; i < newValues.length; i++) {
newValues[i] = fn.apply(((String[]) value)[i]);
}
value = newValues;
} else if (value instanceof List) {
// Apply the transformation to a list of strings
List<Object> newValues = new ArrayList<>(((List) value).size());
for (Object v : (List) value) {
if (v instanceof String) {
newValues.add(fn.apply((String) v));
} else {
newValues.add(v);
}
}
value = newValues;
}
// Apply the transformation to the key
String key = fn.apply(defaultEntry.getKey());
// Copy the value directly in the map if it does not exist yet.
// We don't try to merge maps or list.
if (fields.containsKey(key) == false) {
fields.put(key, value);
}
}
}
return fields;
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.common.http.HttpProxy;
import org.elasticsearch.xpack.notification.jira.JiraIssue;
import org.elasticsearch.xpack.watcher.actions.Action;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
public class JiraAction implements Action {
public static final String TYPE = "jira";
@Nullable final String account;
@Nullable final HttpProxy proxy;
final Map<String, Object> fields;
public JiraAction(@Nullable String account, Map<String, Object> fields, HttpProxy proxy) {
this.account = account;
this.fields = fields;
this.proxy = proxy;
}
@Override
public String type() {
return TYPE;
}
public String getAccount() {
return account;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JiraAction that = (JiraAction) o;
return Objects.equals(account, that.account) &&
Objects.equals(fields, that.fields) &&
Objects.equals(proxy, that.proxy);
}
@Override
public int hashCode() {
return Objects.hash(account, fields, proxy);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (account != null) {
builder.field(Field.ACCOUNT.getPreferredName(), account);
}
if (proxy != null) {
proxy.toXContent(builder, params);
}
builder.field(Field.FIELDS.getPreferredName(), fields);
return builder.endObject();
}
public static JiraAction parse(String watchId, String actionId, XContentParser parser) throws IOException {
String account = null;
HttpProxy proxy = null;
Map<String, Object> fields = null;
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.ACCOUNT)) {
if (token == XContentParser.Token.VALUE_STRING) {
account = parser.text();
} else {
throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. expected [{}] to be of type string, but " +
"found [{}] instead", TYPE, watchId, actionId, Field.ACCOUNT.getPreferredName(), token);
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.PROXY)) {
proxy = HttpProxy.parse(parser);
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.FIELDS)) {
try {
fields = parser.map();
} catch (Exception e) {
throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. failed to parse [{}] field", e, TYPE,
watchId, actionId, Field.FIELDS.getPreferredName());
}
} else {
throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. unexpected token [{}/{}]", TYPE, watchId,
actionId, token, currentFieldName);
}
}
if (fields == null) {
fields = Collections.emptyMap();
}
return new JiraAction(account, fields, proxy);
}
static class Executed extends Action.Result {
private final JiraIssue result;
public Executed(JiraIssue result) {
super(TYPE, result.successful() ? Status.SUCCESS : Status.FAILURE);
this.result = result;
}
public JiraIssue getResult() {
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field(type, result, params);
}
}
static Executed executedResult(JiraIssue result) {
return new Executed(result);
}
static class Simulated extends Action.Result {
private final Map<String, Object> fields;
protected Simulated(Map<String, Object> fields) {
super(TYPE, Status.SIMULATED);
this.fields = fields;
}
public Map<String, Object> getFields() {
return fields;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject(type)
.field(Field.FIELDS.getPreferredName(), fields)
.endObject();
}
}
public static class Builder implements Action.Builder<JiraAction> {
final JiraAction action;
public Builder(JiraAction action) {
this.action = action;
}
@Override
public JiraAction build() {
return action;
}
}
public static Builder builder(String account, Map<String, Object> fields) {
return new Builder(new JiraAction(account, fields, null));
}
public interface Field {
ParseField ACCOUNT = new ParseField("account");
ParseField PROXY = new ParseField("proxy");
ParseField FIELDS = new ParseField("fields");
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.watcher.actions.ActionFactory;
import java.io.IOException;
public class JiraActionFactory extends ActionFactory {
private final TextTemplateEngine templateEngine;
private final JiraService jiraService;
public JiraActionFactory(Settings settings, TextTemplateEngine templateEngine, JiraService jiraService) {
super(Loggers.getLogger(ExecutableJiraAction.class, settings));
this.templateEngine = templateEngine;
this.jiraService = jiraService;
}
@Override
public ExecutableJiraAction parseExecutable(String watchId, String actionId, XContentParser parser) throws IOException {
JiraAction action = JiraAction.parse(watchId, actionId, parser);
jiraService.getAccount(action.getAccount()); // for validation -- throws exception if account not present
return new ExecutableJiraAction(action, actionLogger, jiraService, templateEngine);
}
}

View File

@ -9,9 +9,8 @@ import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.watcher.actions.ActionFactory;
import org.elasticsearch.xpack.watcher.actions.hipchat.ExecutableHipChatAction;
import org.elasticsearch.xpack.notification.pagerduty.PagerDutyService;
import org.elasticsearch.xpack.watcher.actions.ActionFactory;
import java.io.IOException;
@ -21,7 +20,7 @@ public class PagerDutyActionFactory extends ActionFactory {
private final PagerDutyService pagerDutyService;
public PagerDutyActionFactory(Settings settings, TextTemplateEngine templateEngine, PagerDutyService pagerDutyService) {
super(Loggers.getLogger(ExecutableHipChatAction.class, settings));
super(Loggers.getLogger(ExecutablePagerDutyAction.class, settings));
this.templateEngine = templateEngine;
this.pagerDutyService = pagerDutyService;
}

View File

@ -9,9 +9,8 @@ import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.watcher.actions.ActionFactory;
import org.elasticsearch.xpack.watcher.actions.hipchat.ExecutableHipChatAction;
import org.elasticsearch.xpack.notification.slack.SlackService;
import org.elasticsearch.xpack.watcher.actions.ActionFactory;
import java.io.IOException;
@ -20,7 +19,7 @@ public class SlackActionFactory extends ActionFactory {
private final SlackService slackService;
public SlackActionFactory(Settings settings, TextTemplateEngine templateEngine, SlackService slackService) {
super(Loggers.getLogger(ExecutableHipChatAction.class, settings));
super(Loggers.getLogger(ExecutableSlackAction.class, settings));
this.templateEngine = templateEngine;
this.slackService = slackService;
}

View File

@ -0,0 +1,265 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.notification.jira;
import org.apache.http.HttpStatus;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.http.HttpClient;
import org.elasticsearch.xpack.common.http.HttpRequest;
import org.elasticsearch.xpack.common.http.HttpResponse;
import org.elasticsearch.xpack.common.http.Scheme;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.common.collect.Tuple.tuple;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isOneOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JiraAccountTests extends ESTestCase {
private HttpClient httpClient;
private ClusterSettings clusterSettings;
@Before
public void init() throws Exception {
httpClient = mock(HttpClient.class);
clusterSettings = new ClusterSettings(Settings.EMPTY, Collections.singleton(JiraService.JIRA_ACCOUNT_SETTING));
}
public void testJiraAccountSettings() {
final String url = "https://internal-jira.elastic.co:443";
SettingsException e = expectThrows(SettingsException.class, () -> new JiraAccount(null, Settings.EMPTY, null));
assertThat(e.getMessage(), containsString("invalid jira [null] account settings. missing required [url] setting"));
Settings settings1 = Settings.builder().put("url", url).build();
e = expectThrows(SettingsException.class, () -> new JiraAccount("test", settings1, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. missing required [user] setting"));
Settings settings2 = Settings.builder().put("url", url).put("user", "").build();
e = expectThrows(SettingsException.class, () -> new JiraAccount("test", settings2, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. missing required [user] setting"));
Settings settings3 = Settings.builder().put("url", url).put("user", "foo").build();
e = expectThrows(SettingsException.class, () -> new JiraAccount("test", settings3, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. missing required [password] setting"));
Settings settings4 = Settings.builder().put("url", url).put("user", "foo").put("password", "").build();
e = expectThrows(SettingsException.class, () -> new JiraAccount("test", settings4, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. missing required [password] setting"));
}
public void testSingleAccount() throws Exception {
Settings.Builder builder = Settings.builder().put("xpack.notification.jira.default_account", "account1");
addAccountSettings("account1", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
account = service.getAccount(null); // falling back on the default
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
}
public void testSingleAccountNoExplicitDefault() throws Exception {
Settings.Builder builder = Settings.builder();
addAccountSettings("account1", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
account = service.getAccount(null); // falling back on the default
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
}
public void testMultipleAccounts() throws Exception {
Settings.Builder builder = Settings.builder().put("xpack.notification.jira.default_account", "account1");
addAccountSettings("account1", builder);
addAccountSettings("account2", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
account = service.getAccount("account2");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account2"));
account = service.getAccount(null); // falling back on the default
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
}
public void testMultipleAccountsNoExplicitDefault() throws Exception {
Settings.Builder builder = Settings.builder().put("xpack.notification.jira.default_account", "account1");
addAccountSettings("account1", builder);
addAccountSettings("account2", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account1"));
account = service.getAccount("account2");
assertThat(account, notNullValue());
assertThat(account.getName(), equalTo("account2"));
account = service.getAccount(null);
assertThat(account, notNullValue());
assertThat(account.getName(), isOneOf("account1", "account2"));
}
public void testMultipleAccountsUnknownDefault() throws Exception {
Settings.Builder builder = Settings.builder().put("xpack.notification.jira.default_account", "unknown");
addAccountSettings("account1", builder);
addAccountSettings("account2", builder);
SettingsException e = expectThrows(SettingsException.class, () -> new JiraService(builder.build(), httpClient, clusterSettings)
);
assertThat(e.getMessage(), is("could not find default account [unknown]"));
}
public void testNoAccount() throws Exception {
Settings.Builder builder = Settings.builder();
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> service.getAccount(null));
assertThat(e.getMessage(), is("no account found for name: [null]"));
}
public void testNoAccountWithDefaultAccount() throws Exception {
Settings.Builder builder = Settings.builder().put("xpack.notification.jira.default_account", "unknown");
SettingsException e = expectThrows(SettingsException.class, () -> new JiraService(builder.build(), httpClient, clusterSettings)
);
assertThat(e.getMessage(), is("could not find default account [unknown]"));
}
public void testUnsecureAccountUrl() throws Exception {
Settings settings = Settings.builder().put("url", "http://localhost").put("user", "foo").put("password", "bar").build();
SettingsException e = expectThrows(SettingsException.class, () -> new JiraAccount("test", settings, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. unsecure scheme [HTTP]"));
Settings disallowHttp = Settings.builder().put(settings).put("allow_http", false).build();
e = expectThrows(SettingsException.class, () -> new JiraAccount("test", disallowHttp, null));
assertThat(e.getMessage(), containsString("invalid jira [test] account settings. unsecure scheme [HTTP]"));
Settings allowHttp = Settings.builder().put(settings).put("allow_http", true).build();
assertNotNull(new JiraAccount("test", allowHttp, null));
}
public void testCreateIssueWithError() throws Exception {
Settings.Builder builder = Settings.builder();
addAccountSettings("account1", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
Tuple<Integer, String> error = randomHttpError();
when(httpClient.execute(any(HttpRequest.class))).thenReturn(new HttpResponse(error.v1()));
JiraIssue issue = account.createIssue(emptyMap(), null);
assertFalse(issue.successful());
assertThat(issue.getFailureReason(), equalTo(error.v2()));
}
public void testCreateIssue() throws Exception {
Settings.Builder builder = Settings.builder();
addAccountSettings("account1", builder);
JiraService service = new JiraService(builder.build(), httpClient, clusterSettings);
JiraAccount account = service.getAccount("account1");
ArgumentCaptor<HttpRequest> argumentCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.execute(argumentCaptor.capture())).thenReturn(new HttpResponse(HttpStatus.SC_CREATED));
Map<String, Object> fields = singletonMap("key", "value");
JiraIssue issue = account.createIssue(fields, null);
assertTrue(issue.successful());
assertNull(issue.getFailureReason());
HttpRequest sentRequest = argumentCaptor.getValue();
assertThat(sentRequest.host(), equalTo("internal-jira.elastic.co"));
assertThat(sentRequest.port(), equalTo(443));
assertThat(sentRequest.scheme(), equalTo(Scheme.HTTPS));
assertThat(sentRequest.path(), equalTo(JiraAccount.DEFAULT_PATH));
assertThat(sentRequest.auth(), notNullValue());
assertThat(sentRequest.body(), notNullValue());
}
private void addAccountSettings(String name, Settings.Builder builder) {
builder.put("xpack.notification.jira.account." + name + "." + JiraAccount.URL_SETTING, "https://internal-jira.elastic.co:443");
builder.put("xpack.notification.jira.account." + name + "." + JiraAccount.USER_SETTING, randomAsciiOfLength(10));
builder.put("xpack.notification.jira.account." + name + "." + JiraAccount.PASSWORD_SETTING, randomAsciiOfLength(10));
Map<String, Object> defaults = randomIssueDefaults();
for (Map.Entry<String, Object> setting : defaults.entrySet()) {
String key = "xpack.notification.jira.account." + name + "." + JiraAccount.ISSUE_DEFAULTS_SETTING + "." + setting.getKey();
if (setting.getValue() instanceof String) {
builder.put(key, setting.getValue());
} else if (setting.getValue() instanceof Map) {
builder.putProperties((Map) setting.getValue(), s -> true, s -> key + "." + s);
}
}
}
public static Map<String, Object> randomIssueDefaults() {
MapBuilder<String, Object> builder = MapBuilder.newMapBuilder();
if (randomBoolean()) {
Map<String, Object> project = new HashMap<>();
project.put("project", singletonMap("id", randomAsciiOfLength(10)));
builder.putAll(project);
}
if (randomBoolean()) {
Map<String, Object> project = new HashMap<>();
project.put("issuetype", singletonMap("name", randomAsciiOfLength(5)));
builder.putAll(project);
}
if (randomBoolean()) {
builder.put("summary", randomAsciiOfLength(10));
}
if (randomBoolean()) {
builder.put("description", randomAsciiOfLength(50));
}
if (randomBoolean()) {
int count = randomIntBetween(0, 5);
for (int i = 0; i < count; i++) {
builder.put("customfield_" + i, randomAsciiOfLengthBetween(5, 10));
}
}
return builder.immutableMap();
}
static Tuple<Integer, String> randomHttpError() {
Tuple<Integer, String> error = randomFrom(
tuple(400, "Bad Request"),
tuple(401, "Unauthorized (authentication credentials are invalid)"),
tuple(403, "Forbidden (account doesn't have permission to create this issue)"),
tuple(404, "Not Found (account uses invalid JIRA REST APIs)"),
tuple(408, "Request Timeout (request took too long to process)"),
tuple(500, "JIRA Server Error (internal error occurred while processing request)"),
tuple(666, "Unknown Error")
);
return error;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.notification.jira;
import org.apache.http.HttpStatus;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.http.HttpMethod;
import org.elasticsearch.xpack.common.http.HttpRequest;
import org.elasticsearch.xpack.common.http.HttpResponse;
import org.elasticsearch.xpack.common.http.auth.HttpAuthRegistry;
import org.elasticsearch.xpack.common.http.auth.basic.BasicAuth;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static org.elasticsearch.common.xcontent.XContentFactory.cborBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.smileBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.yamlBuilder;
import static org.elasticsearch.xpack.notification.jira.JiraAccountTests.randomHttpError;
import static org.elasticsearch.xpack.notification.jira.JiraAccountTests.randomIssueDefaults;
import static org.elasticsearch.xpack.notification.jira.JiraIssue.resolveFailureReason;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.mock;
public class JiraIssueTests extends ESTestCase {
public void testToXContent() throws Exception {
final JiraIssue issue = randomJiraIssue();
BytesReference bytes = null;
try (XContentBuilder builder = randomFrom(jsonBuilder(), smileBuilder(), yamlBuilder(), cborBuilder())) {
issue.toXContent(builder, ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
}
Map<String, Object> parsedFields = null;
Map<String, Object> parsedResult = null;
HttpRequest parsedRequest = null;
HttpResponse parsedResponse = null;
String parsedReason = null;
try (XContentParser parser = XContentHelper.createParser(bytes)) {
assertNull(parser.currentToken());
parser.nextToken();
XContentParser.Token token = parser.currentToken();
assertThat(token, is(XContentParser.Token.START_OBJECT));
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if ("result".equals(currentFieldName)) {
parsedResult = parser.map();
} else if ("request".equals(currentFieldName)) {
HttpRequest.Parser httpRequestParser = new HttpRequest.Parser(mock(HttpAuthRegistry.class));
parsedRequest = httpRequestParser.parse(parser);
} else if ("response".equals(currentFieldName)) {
parsedResponse = HttpResponse.parse(parser);
} else if ("fields".equals(currentFieldName)) {
parsedFields = parser.map();
} else if ("reason".equals(currentFieldName)) {
parsedReason = parser.text();
} else {
fail("unknown field [" + currentFieldName + "]");
}
}
}
assertThat(parsedFields, equalTo(issue.getFields()));
if (issue.successful()) {
assertThat(parsedResult, hasEntry("key", "TEST"));
assertNull(parsedRequest);
assertNull(parsedResponse);
} else {
assertThat(parsedRequest, equalTo(issue.getRequest()));
assertThat(parsedResponse, equalTo(issue.getResponse()));
assertThat(parsedReason, equalTo(resolveFailureReason(issue.getResponse())));
}
}
public void testEquals() throws Exception {
final JiraIssue issue1 = randomJiraIssue();
final boolean equals = randomBoolean();
final Map<String, Object> fields = new HashMap<>(issue1.getFields());
if (equals == false) {
String key = randomFrom(fields.keySet());
fields.remove(key);
}
JiraIssue issue2 = new JiraIssue(fields, issue1.getRequest(), issue1.getResponse(), issue1.getFailureReason());
assertThat(issue1.equals(issue2), is(equals));
}
private static JiraIssue randomJiraIssue() throws IOException {
Map<String, Object> fields = randomIssueDefaults();
HttpRequest request = HttpRequest.builder(randomFrom("localhost", "internal-jira.elastic.co"), randomFrom(80, 443))
.method(HttpMethod.POST)
.path(JiraAccount.DEFAULT_PATH)
.auth(new BasicAuth(randomAsciiOfLength(5), randomAsciiOfLength(5).toCharArray()))
.build();
if (rarely()) {
Tuple<Integer, String> error = randomHttpError();
return JiraIssue.responded(fields, request, new HttpResponse(error.v1(), "{\"error\": \"" + error.v2() + "\"}"));
}
return JiraIssue.responded(fields, request, new HttpResponse(HttpStatus.SC_CREATED, "{\"key\": \"TEST\"}"));
}
}

View File

@ -0,0 +1,313 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.http.HttpClient;
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;
import org.elasticsearch.xpack.common.http.auth.basic.BasicAuth;
import org.elasticsearch.xpack.common.text.TextTemplate;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.jira.JiraAccount;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.watcher.actions.Action;
import org.elasticsearch.xpack.watcher.execution.WatchExecutionContext;
import org.elasticsearch.xpack.watcher.execution.Wid;
import org.elasticsearch.xpack.watcher.watch.Payload;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.xpack.watcher.test.WatcherTestUtils.mockExecutionContextBuilder;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ExecutableJiraActionTests extends ESTestCase {
public void testProxy() throws Exception {
HttpProxy proxy = new HttpProxy("localhost", 8080);
Map<String, Object> issueDefaults = Collections.singletonMap("customfield_0001", "test");
JiraAction action = new JiraAction("account1", issueDefaults, proxy);
HttpClient httpClient = mock(HttpClient.class);
ArgumentCaptor<HttpRequest> argumentCaptor = ArgumentCaptor.forClass(HttpRequest.class);
when(httpClient.execute(argumentCaptor.capture())).thenReturn(new HttpResponse(200));
final String host = randomFrom("localhost", "internal-jira.elastic.co");
final int port = randomFrom(80, 8080, 449, 9443);
final String url = "https://" + host + ":" + port;
final String user = randomAsciiOfLength(10);
final String password = randomAsciiOfLength(10);
Settings accountSettings = Settings.builder()
.put("url", url)
.put("user", user)
.put("password", password)
.build();
JiraAccount account = new JiraAccount("account1", accountSettings, httpClient);
JiraService service = mock(JiraService.class);
when(service.getAccount(eq("account1"))).thenReturn(account);
DateTime now = DateTime.now(DateTimeZone.UTC);
Wid wid = new Wid(randomAsciiOfLength(5), randomLong(), now);
WatchExecutionContext ctx = mockExecutionContextBuilder(wid.watchId())
.wid(wid)
.payload(new Payload.Simple())
.time(wid.watchId(), now)
.buildMock();
ExecutableJiraAction executable = new ExecutableJiraAction(action, logger, service, new UpperCaseTextTemplateEngine());
executable.execute("foo", ctx, new Payload.Simple());
HttpRequest request = argumentCaptor.getValue();
assertThat(request.proxy(), is(proxy));
assertThat(request.host(), is(host));
assertThat(request.port(), is(port));
assertThat(request.path(), is(JiraAccount.DEFAULT_PATH));
HttpAuth httpAuth = request.auth();
assertThat(httpAuth.type(), is("basic"));
BasicAuth basicAuth = (BasicAuth) httpAuth;
assertThat(basicAuth.getUsername(), is(user));
}
public void testExecutionWithNoDefaults() throws Exception {
JiraAction.Simulated result = simulateExecution(singletonMap("key", "value"), emptyMap());
assertEquals(result.getFields().size(), 1);
assertThat(result.getFields(), hasEntry("KEY", "VALUE"));
}
public void testExecutionNoFieldsWithDefaults() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0", "v0");
JiraAction.Simulated result = simulateExecution(new HashMap<>(), defaults);
assertEquals(result.getFields().size(), 1);
assertThat(result.getFields(), hasEntry("K0", "V0"));
defaults.put("k1", "v1");
result = simulateExecution(new HashMap<>(), defaults);
assertEquals(result.getFields().size(), 2);
assertThat(result.getFields(), allOf(hasEntry("K0", "V0"), hasEntry("K1", "V1")));
}
public void testExecutionFields() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0", "v0");
defaults.put("k1", "v1");
Map<String, Object> fields = new HashMap<>();
fields.put("k1", "new_v1"); // overridden
fields.put("k2", "v2");
fields.put("k3", "v3");
JiraAction.Simulated result = simulateExecution(fields, defaults);
assertEquals(result.getFields().size(), 4);
assertThat(result.getFields(), allOf(hasEntry("K0", "V0"), hasEntry("K1", "NEW_V1"), hasEntry("K2", "V2"), hasEntry("K3", "V3")));
}
public void testExecutionFieldsMaps() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0.a", "b");
defaults.put("k1.c", "d");
defaults.put("k1.e", "f");
defaults.put("k1.g.a", "b");
Map<String, Object> fields = new HashMap<>();
fields.put("k2", "v2");
fields.put("k3", "v3");
JiraAction.Simulated result = simulateExecution(fields, defaults);
final Map<String, Object> expected = new HashMap<>();
expected.put("K0", singletonMap("A", "B"));
expected.put("K2", "V2");
expected.put("K3", "V3");
final Map<String, Object> expectedK1 = new HashMap<>();
expectedK1.put("C", "D");
expectedK1.put("E", "F");
expectedK1.put("G", singletonMap("A", "B"));
expected.put("K1", expectedK1);
assertThat(result.getFields(), equalTo(expected));
}
public void testExecutionFieldsMapsAreOverridden() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0", "v0");
defaults.put("k1.a", "b");
defaults.put("k1.c", "d");
Map<String, Object> fields = new HashMap<>();
fields.put("k1", singletonMap("c", "e")); // will overrides the defaults
fields.put("k2", "v2");
JiraAction.Simulated result = simulateExecution(fields, defaults);
final Map<String, Object> expected = new HashMap<>();
expected.put("K0", "V0");
expected.put("K1", singletonMap("C", "E"));
expected.put("K2", "V2");
assertThat(result.getFields(), equalTo(expected));
}
public void testExecutionFieldsLists() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0.0", "a");
defaults.put("k0.1", "b");
defaults.put("k0.2", "c");
defaults.put("k1", "v1");
Map<String, Object> fields = new HashMap<>();
fields.put("k2", "v2");
fields.put("k3", Arrays.asList("d", "e", "f"));
JiraAction.Simulated result = simulateExecution(fields, defaults);
final Map<String, Object> expected = new HashMap<>();
expected.put("K0", Arrays.asList("A", "B", "C"));
expected.put("K1", "V1");
expected.put("K2", "V2");
expected.put("K3", Arrays.asList("D", "E", "F"));
assertThat(result.getFields(), equalTo(expected));
}
public void testExecutionFieldsListsNotOverridden() throws Exception {
Map<String, String> defaults = new HashMap<>();
defaults.put("k0.0", "a");
defaults.put("k0.1", "b");
defaults.put("k0.2", "c");
Map<String, Object> fields = new HashMap<>();
fields.put("k1", "v1");
fields.put("k0", Arrays.asList("d", "e", "f")); // should not be overridden byt the defaults
JiraAction.Simulated result = simulateExecution(fields, defaults);
final Map<String, Object> expected = new HashMap<>();
expected.put("K0", Arrays.asList("D", "E", "F"));
expected.put("K1", "V1");
assertThat(result.getFields(), equalTo(expected));
}
public void testExecutionFieldsStringArrays() throws Exception {
Map<String, String> defaults = Settings.builder()
.putArray("k0", "a", "b", "c")
.put("k1", "v1")
.build()
.getAsMap();
Map<String, Object> fields = new HashMap<>();
fields.put("k2", "v2");
fields.put("k3", new String[]{"d", "e", "f"});
JiraAction.Simulated result = simulateExecution(fields, defaults);
assertThat(result.getFields().get("K1"), equalTo("V1"));
assertThat(result.getFields().get("K2"), equalTo("V2"));
assertArrayEquals((Object[]) result.getFields().get("K3"), new Object[]{"D", "E", "F"});
}
public void testExecutionFieldsStringArraysNotOverridden() throws Exception {
Map<String, String> defaults = Settings.builder()
.putArray("k0", "a", "b", "c")
.build()
.getAsMap();
Map<String, Object> fields = new HashMap<>();
fields.put("k1", "v1");
fields.put("k0", new String[]{"d", "e", "f"}); // should not be overridden byt the defaults
JiraAction.Simulated result = simulateExecution(fields, defaults);
final Map<String, Object> expected = new HashMap<>();
expected.put("K0", new String[]{"D", "E", "F"});
expected.put("K1", "V1");
assertArrayEquals((Object[]) result.getFields().get("K0"), new Object[]{"D", "E", "F"});
assertThat(result.getFields().get("K1"), equalTo("V1"));
}
private JiraAction.Simulated simulateExecution(Map<String, Object> actionFields, Map<String, String> accountFields) throws Exception {
Settings.Builder settings = Settings.builder()
.put("url", "https://internal-jira.elastic.co:443")
.put("user", "elastic")
.put("password", "secret")
.putProperties(accountFields, s -> true, s -> "issue_defaults." + s);
JiraAccount account = new JiraAccount("account", settings.build(), mock(HttpClient.class));
JiraService service = mock(JiraService.class);
when(service.getAccount(eq("account"))).thenReturn(account);
JiraAction action = new JiraAction("account", actionFields, null);
ExecutableJiraAction executable = new ExecutableJiraAction(action, null, service, new UpperCaseTextTemplateEngine());
WatchExecutionContext context = createWatchExecutionContext();
when(context.simulateAction("test")).thenReturn(true);
Action.Result result = executable.execute("test", context, new Payload.Simple());
assertThat(result, instanceOf(JiraAction.Result.class));
assertThat(result, instanceOf(JiraAction.Simulated.class));
return (JiraAction.Simulated) result;
}
private WatchExecutionContext createWatchExecutionContext() {
DateTime now = DateTime.now(DateTimeZone.UTC);
Wid wid = new Wid(randomAsciiOfLength(5), randomLong(), now);
Map<String, Object> metadata = MapBuilder.<String, Object>newMapBuilder().put("_key", "_val").map();
return mockExecutionContextBuilder("watch1")
.wid(wid)
.payload(new Payload.Simple())
.time("watch1", now)
.metadata(metadata)
.buildMock();
}
/**
* TextTemplateEngine that convert templates to uppercase
*/
class UpperCaseTextTemplateEngine extends TextTemplateEngine {
public UpperCaseTextTemplateEngine() {
super(Settings.EMPTY, mock(ScriptService.class));
}
@Override
public String render(TextTemplate textTemplate, Map<String, Object> model) {
return textTemplate.getTemplate().toUpperCase(Locale.ROOT);
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.jira.JiraAccount;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.junit.Before;
import static java.util.Collections.singleton;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.xpack.notification.jira.JiraAccountTests.randomIssueDefaults;
import static org.elasticsearch.xpack.watcher.actions.ActionBuilders.jiraAction;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JiraActionFactoryTests extends ESTestCase {
private JiraActionFactory factory;
private JiraService service;
@Before
public void init() throws Exception {
service = mock(JiraService.class);
factory = new JiraActionFactory(Settings.EMPTY, mock(TextTemplateEngine.class), service);
}
public void testParseAction() throws Exception {
JiraAccount account = mock(JiraAccount.class);
when(service.getAccount("_account1")).thenReturn(account);
JiraAction action = jiraAction("_account1", randomIssueDefaults()).build();
XContentBuilder jsonBuilder = jsonBuilder().value(action);
XContentParser parser = JsonXContent.jsonXContent.createParser(jsonBuilder.bytes());
parser.nextToken();
JiraAction parsedAction = JiraAction.parse("_w1", "_a1", parser);
assertThat(parsedAction, equalTo(action));
}
public void testParseActionUnknownAccount() throws Exception {
ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, singleton(JiraService.JIRA_ACCOUNT_SETTING));
JiraService service = new JiraService(Settings.EMPTY, null, clusterSettings);
factory = new JiraActionFactory(Settings.EMPTY, mock(TextTemplateEngine.class), service);
JiraAction action = jiraAction("_unknown", randomIssueDefaults()).build();
XContentBuilder jsonBuilder = jsonBuilder().value(action);
XContentParser parser = JsonXContent.jsonXContent.createParser(jsonBuilder.bytes());
parser.nextToken();
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> factory.parseExecutable("_w1", "_a1", parser));
assertThat(e.getMessage(), containsString("no account found for name: [_unknown]"));
}
}

View File

@ -0,0 +1,311 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.watcher.actions.jira;
import org.apache.http.HttpStatus;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.common.http.HttpClient;
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.text.TextTemplate;
import org.elasticsearch.xpack.common.text.TextTemplateEngine;
import org.elasticsearch.xpack.notification.jira.JiraAccount;
import org.elasticsearch.xpack.notification.jira.JiraAccountTests;
import org.elasticsearch.xpack.notification.jira.JiraIssue;
import org.elasticsearch.xpack.notification.jira.JiraService;
import org.elasticsearch.xpack.watcher.actions.Action;
import org.elasticsearch.xpack.watcher.execution.WatchExecutionContext;
import org.elasticsearch.xpack.watcher.execution.Wid;
import org.elasticsearch.xpack.watcher.support.Variables;
import org.elasticsearch.xpack.watcher.watch.Payload;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Before;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.common.xcontent.XContentFactory.cborBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.smileBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.yamlBuilder;
import static org.elasticsearch.xpack.watcher.test.WatcherTestUtils.mockExecutionContextBuilder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JiraActionTests extends ESTestCase {
private JiraService service;
@Before
public void init() throws Exception {
service = mock(JiraService.class);
}
public void testParser() throws Exception {
final String accountName = randomAsciiOfLength(10);
final Map<String, Object> issueDefaults = JiraAccountTests.randomIssueDefaults();
XContentBuilder builder = jsonBuilder().startObject()
.field("account", accountName)
.field("fields", issueDefaults)
.endObject();
BytesReference bytes = builder.bytes();
logger.info("jira action json [{}]", bytes.utf8ToString());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
JiraAction action = JiraAction.parse("_watch", "_action", parser);
assertThat(action, notNullValue());
assertThat(action.account, is(accountName));
assertThat(action.fields, notNullValue());
assertThat(action.fields, is(issueDefaults));
}
public void testParserSelfGenerated() throws Exception {
final JiraAction action = randomJiraAction();
XContentBuilder builder = jsonBuilder();
action.toXContent(builder, ToXContent.EMPTY_PARAMS);
BytesReference bytes = builder.bytes();
logger.info("{}", bytes.utf8ToString());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
JiraAction parsedAction = JiraAction.parse("_watch", "_action", parser);
assertThat(parsedAction, notNullValue());
assertThat(parsedAction.proxy, equalTo(action.proxy));
assertThat(parsedAction.fields, equalTo(action.fields));
assertThat(parsedAction.account, equalTo(action.account));
assertThat(parsedAction, is(action));
}
public void testParserInvalid() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().field("unknown_field", "value").endObject();
XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes());
parser.nextToken();
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> JiraAction.parse("_w", "_a", parser));
assertThat(e.getMessage(), is("failed to parse [jira] action [_w/_a]. unexpected token [VALUE_STRING/unknown_field]"));
}
public void testToXContent() throws Exception {
final JiraAction action = randomJiraAction();
BytesReference bytes = null;
try (XContentBuilder builder = randomFrom(jsonBuilder(), smileBuilder(), yamlBuilder(), cborBuilder())) {
action.toXContent(builder, ToXContent.EMPTY_PARAMS);
bytes = builder.bytes();
}
String parsedAccount = null;
HttpProxy parsedProxy = null;
Map<String, Object> parsedFields = null;
try (XContentParser parser = XContentHelper.createParser(bytes)) {
assertNull(parser.currentToken());
parser.nextToken();
XContentParser.Token token = parser.currentToken();
assertThat(token, is(XContentParser.Token.START_OBJECT));
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if ("account".equals(currentFieldName)) {
parsedAccount = parser.text();
} else if ("proxy".equals(currentFieldName)) {
parsedProxy = HttpProxy.parse(parser);
} else if ("fields".equals(currentFieldName)) {
parsedFields = parser.map();
} else {
fail("unknown field [" + currentFieldName + "]");
}
}
}
assertThat(parsedAccount, equalTo(action.getAccount()));
assertThat(parsedProxy, equalTo(action.proxy));
assertThat(parsedFields, equalTo(action.fields));
}
public void testEquals() throws Exception {
final JiraAction action1 = randomJiraAction();
String account = action1.account;
Map<String, Object> fields = action1.fields;
HttpProxy proxy = action1.proxy;
boolean equals = randomBoolean();
if (!equals) {
equals = true;
if (rarely()) {
equals = false;
account = "another account";
}
if (rarely()) {
equals = false;
fields = JiraAccountTests.randomIssueDefaults();
}
if (rarely()) {
equals = false;
proxy = randomHttpProxy();
}
}
JiraAction action2 = new JiraAction(account, fields, proxy);
assertThat(action1.equals(action2), is(equals));
}
public void testExecute() throws Exception {
final Map<String, Object> model = new HashMap<>();
final MapBuilder<String, Object> actionFields = MapBuilder.newMapBuilder();
String summary = randomAsciiOfLength(15);
actionFields.put("summary", "{{ctx.summary}}");
model.put("{{ctx.summary}}", summary);
String projectId = randomAsciiOfLength(10);
actionFields.put("project", singletonMap("id", "{{ctx.project_id}}"));
model.put("{{ctx.project_id}}", projectId);
String description = null;
if (randomBoolean()) {
description = randomAsciiOfLength(50);
actionFields.put("description", description);
}
String issueType = null;
if (randomBoolean()) {
issueType = randomFrom("Bug", "Test", "Task", "Epic");
actionFields.put("issuetype", singletonMap("name", issueType));
}
String watchId = null;
if (randomBoolean()) {
watchId = "jira_watch_" + randomInt();
model.put("{{" + Variables.WATCH_ID + "}}", watchId);
actionFields.put("customfield_0", "{{watch_id}}");
}
HttpClient httpClient = mock(HttpClient.class);
when(httpClient.execute(any(HttpRequest.class))).thenReturn(new HttpResponse(HttpStatus.SC_CREATED));
Settings.Builder settings = Settings.builder()
.put("url", "https://internal-jira.elastic.co:443")
.put("user", "elastic")
.put("password", "secret")
.put("issue_defaults.customfield_000", "foo")
.put("issue_defaults.customfield_001", "bar");
JiraAccount account = new JiraAccount("account", settings.build(), httpClient);
JiraService service = mock(JiraService.class);
when(service.getAccount(eq("account"))).thenReturn(account);
JiraAction action = new JiraAction("account", actionFields.immutableMap(), null);
ExecutableJiraAction executable = new ExecutableJiraAction(action, logger, service, new ModelTextTemplateEngine(model));
Map<String, Object> data = new HashMap<>();
Payload payload = new Payload.Simple(data);
DateTime now = DateTime.now(DateTimeZone.UTC);
Wid wid = new Wid(randomAsciiOfLength(5), randomLong(), now);
WatchExecutionContext context = mockExecutionContextBuilder(wid.watchId())
.wid(wid)
.payload(payload)
.time(wid.watchId(), now)
.buildMock();
when(context.simulateAction("test")).thenReturn(false);
Action.Result result = executable.execute("test", context, new Payload.Simple());
assertThat(result, instanceOf(JiraAction.Result.class));
assertThat(result, instanceOf(JiraAction.Executed.class));
JiraIssue issue = ((JiraAction.Executed) result).getResult();
assertThat(issue.getFields().get("summary"), equalTo(summary));
assertThat(issue.getFields().get("customfield_000"), equalTo("foo"));
assertThat(issue.getFields().get("customfield_001"), equalTo("bar"));
assertThat(((Map) issue.getFields().get("project")).get("id"), equalTo(projectId));
if (issueType != null) {
assertThat(((Map) issue.getFields().get("issuetype")).get("name"), equalTo(issueType));
}
if (description != null) {
assertThat(issue.getFields().get("description"), equalTo(description));
}
if (watchId != null) {
assertThat(issue.getFields().get("customfield_0"), equalTo(watchId));
}
}
private static JiraAction randomJiraAction() {
String account = null;
if (randomBoolean()) {
account = randomAsciiOfLength(randomIntBetween(5, 10));
}
Map<String, Object> fields = emptyMap();
if (frequently()) {
fields = JiraAccountTests.randomIssueDefaults();
}
HttpProxy proxy = null;
if (randomBoolean()) {
proxy = randomHttpProxy();
}
return new JiraAction(account, fields, proxy);
}
private static HttpProxy randomHttpProxy() {
return new HttpProxy(randomFrom("localhost", "www.elastic.co", "198.18.0.0"), randomIntBetween(8000, 10000));
}
/**
* TextTemplateEngine that picks up templates from the model if exist,
* otherwise returns the template as it is.
*/
class ModelTextTemplateEngine extends TextTemplateEngine {
private final Map<String, Object> model;
public ModelTextTemplateEngine(Map<String, Object> model) {
super(Settings.EMPTY, mock(ScriptService.class));
this.model = model;
}
@Override
public String render(TextTemplate textTemplate, Map<String, Object> ignoredModel) {
String template = textTemplate.getTemplate();
if (model.containsKey(template)) {
return (String) model.get(template);
}
return template;
}
}
}

View File

@ -6,6 +6,10 @@ dependencies {
}
integTest {
// JIRA integration tests are ignored until a JIRA server is available for testing
// see https://github.com/elastic/infra/issues/1498
systemProperty 'tests.rest.blacklist', 'actions/20_jira/*'
cluster {
plugin ':x-plugins:elasticsearch'
setting 'xpack.security.enabled', 'false'
@ -13,5 +17,16 @@ integTest {
setting 'http.port', '9400'
setting 'script.inline', 'true'
setting 'script.stored', 'true'
// Need to allow more compilations per minute because of the integration tests
setting 'script.max_compilations_per_minute', '100'
// JIRA integration test settings
setting 'xpack.notification.jira.account.test.url', 'http://localhost:8080'
setting 'xpack.notification.jira.account.test.allow_http', 'true'
setting 'xpack.notification.jira.account.test.user', 'jira_user'
setting 'xpack.notification.jira.account.test.password', 'secret'
setting 'xpack.notification.jira.account.test.issue_defaults.project.key', 'BAS'
setting 'xpack.notification.jira.account.test.issue_defaults.labels', ['integration-tests']
}
}

View File

@ -0,0 +1,159 @@
---
"Test Jira Action":
- do:
cluster.health:
wait_for_status: yellow
- do:
xpack.watcher.put_watch:
id: "jira_watch"
body: >
{
"metadata": {
"custom_title": "Hello from"
},
"trigger": {
"schedule": {
"interval": "1s"
}
},
"input": {
"simple": {
}
},
"condition": {
"always": {}
},
"actions": {
"create_jira_issue": {
"jira": {
"account": "test",
"fields": {
"summary": "{{ctx.metadata.custom_title}} {{ctx.watch_id}}",
"description": "Issue created by the REST integration test [/watcher/actions/10_jira.yaml]",
"issuetype" : {
"name": "Bug"
}
}
}
}
}
}
- match: { _id: "jira_watch" }
- match: { created: true }
- do:
xpack.watcher.execute_watch:
id: "jira_watch"
body: >
{
"trigger_data" : {
"triggered_time" : "2012-12-12T12:12:12.120Z",
"scheduled_time" : "2000-12-12T12:12:12.120Z"
},
"record_execution": true
}
- match: { watch_record.watch_id: "jira_watch" }
- match: { watch_record.trigger_event.type: "manual" }
- match: { watch_record.trigger_event.triggered_time: "2012-12-12T12:12:12.120Z" }
- match: { watch_record.trigger_event.manual.schedule.scheduled_time: "2000-12-12T12:12:12.120Z" }
- match: { watch_record.state: "executed" }
# Waits for the watcher history index to be available
- do:
cluster.health:
index: ".watcher-history-*"
wait_for_no_relocating_shards: true
timeout: 60s
- do:
indices.refresh: {}
- do:
search:
index: ".watcher-history-*"
- match: { hits.total: 1 }
- match: { hits.hits.0._type: "watch_record" }
- match: { hits.hits.0._source.watch_id: "jira_watch" }
- match: { hits.hits.0._source.state: "executed" }
- match: { hits.hits.0._source.result.actions.0.id: "create_jira_issue" }
- match: { hits.hits.0._source.result.actions.0.type: "jira" }
- match: { hits.hits.0._source.result.actions.0.status: "success" }
- match: { hits.hits.0._source.result.actions.0.jira.fields.summary: "Hello from jira_watch" }
- match: { hits.hits.0._source.result.actions.0.jira.fields.issuetype.name: "Bug" }
- match: { hits.hits.0._source.result.actions.0.jira.fields.project.key: "BAS" }
- match: { hits.hits.0._source.result.actions.0.jira.fields.labels.0: "integration-tests" }
- match: { hits.hits.0._source.result.actions.0.jira.result.id: /\d+/ }
- match: { hits.hits.0._source.result.actions.0.jira.result.key: /BAS-\d+/ }
---
"Test Jira Action with Error":
- do:
cluster.health:
wait_for_status: yellow
- do:
xpack.watcher.put_watch:
id: "wrong_jira_watch"
body: >
{
"trigger": {
"schedule": {
"interval": "1d"
}
},
"input": {
"simple": {
}
},
"condition": {
"always": {}
},
"actions": {
"fail_to_create_jira_issue": {
"jira": {
"account": "test",
"fields": {
"summary": "Hello from {{ctx.watch_id}}",
"description": "This Jira issue does not have a type (see below) so it won't be created at all",
"issuetype" : {
"name": null
}
}
}
}
}
}
- match: { _id: "wrong_jira_watch" }
- match: { created: true }
- do:
xpack.watcher.execute_watch:
id: "wrong_jira_watch"
body: >
{
"trigger_data" : {
"triggered_time" : "2012-12-12T12:12:12.120Z",
"scheduled_time" : "2000-12-12T12:12:12.120Z"
}
}
- match: { watch_record.watch_id: "wrong_jira_watch" }
- match: { watch_record.trigger_event.type: "manual" }
- match: { watch_record.trigger_event.triggered_time: "2012-12-12T12:12:12.120Z" }
- match: { watch_record.trigger_event.manual.schedule.scheduled_time: "2000-12-12T12:12:12.120Z" }
- match: { watch_record.state: "executed" }
- match: { watch_record.result.actions.0.id: "fail_to_create_jira_issue" }
- match: { watch_record.result.actions.0.type: "jira" }
- match: { watch_record.result.actions.0.status: "failure" }
- match: { watch_record.result.actions.0.jira.fields.summary: "Hello from wrong_jira_watch" }
- is_false: watch_record.result.actions.0.jira.fields.issuetype.name
- match: { watch_record.result.actions.0.jira.fields.project.key: "BAS" }
- match: { watch_record.result.actions.0.jira.fields.labels.0: "integration-tests" }
- match: { watch_record.result.actions.0.jira.reason: "Bad Request - Field [issuetype] has error [issue type is required]\n" }
- match: { watch_record.result.actions.0.jira.request.method: "post" }
- match: { watch_record.result.actions.0.jira.request.path: "/rest/api/2/issue" }
- match: { watch_record.result.actions.0.jira.response.body: "{\"errorMessages\":[],\"errors\":{\"issuetype\":\"issue type is required\"}}" }