diff --git a/watcher/docs/images/hipchat-integration-example.png b/watcher/docs/images/hipchat-integration-example.png new file mode 100644 index 00000000000..9d8bec6bdf9 Binary files /dev/null and b/watcher/docs/images/hipchat-integration-example.png differ diff --git a/watcher/docs/reference/actions.asciidoc b/watcher/docs/reference/actions.asciidoc index 5ebcc71efb4..0daf8df3c3f 100644 --- a/watcher/docs/reference/actions.asciidoc +++ b/watcher/docs/reference/actions.asciidoc @@ -157,9 +157,9 @@ during its execution: image::images/action-throttling.jpg[align="center"] -Watcher supports four action types: <>, -<>, <> and -<>. +Watcher supports five action types: <>, +<>, <>, +<> and <> include::actions/email.asciidoc[] @@ -167,4 +167,6 @@ include::actions/webhook.asciidoc[] include::actions/index.asciidoc[] -include::actions/logging.asciidoc[] \ No newline at end of file +include::actions/logging.asciidoc[] + +include::actions/hipchat.asciidoc[] \ No newline at end of file diff --git a/watcher/docs/reference/actions/hipchat.asciidoc b/watcher/docs/reference/actions/hipchat.asciidoc new file mode 100644 index 00000000000..121401bbf56 --- /dev/null +++ b/watcher/docs/reference/actions/hipchat.asciidoc @@ -0,0 +1,235 @@ +[[actions-hipchat]] +==== HipChat Action + +A watch <> that can connect to a https://www.hipchat.com[HipChat] server and send +messages to users and rooms of a specific group. + + +[[configuring-hipchat-actions]] +===== Configuring HipChat Actions + +You configure hipchat actions in a watch's `actions` array. Action-specific attributes are +specified using the `hipchat` keyword. + +The following snippet shows a simple hipchat action definition: + +[source,json] +-------------------------------------------------- +"actions" : { + "notify-hipchat" : { <1> + "transform" : { ... }, <2> + "throttle_period" : "5m", <3> + "hipchat" : { + "to" : { + "room" : "server-status" <4> + }, + "message" : "Encountered {{ctx.payload.hits.total}} errors in the last 5 minutes (facepalm)" <5> + } + } +} +-------------------------------------------------- + +<1> The id of the action +<2> An optional <> to transform the payload before executing the `webhook` action +<3> An optional <> for the action (5 minutes in this example) +<4> The room where the message is sent to +<5> The content of the message. + +HipChat provides an extensive set of APIs via which Watcher sends messages to users and rooms. These APIs +are exposed via different integration mechanism. Watcher refers to these as *Profiles*, each can be identified +with its own unique name: `v1`, `integration` and `user`. + +Different profiles support different features and require different set of configuration (both on watcher +side and on the HipChat server side). + +Before using the `hipchat` action in a watch, Watcher's internal HipChat service needs to be configured. This +Service enable the configuration of multiple HipChat accounts. An HipChat Account defines the following: + +* `name` - (required) uniquely identifies the account. HipChat actions may specify the name of the account with which + the messages should be sent. +* `profile` - (required) the profile that is associated with this account, effectively defining what APIs this account uses. +* `auth_token` - (required) the authentication token that is used to execute the HipChat API in the account. +* `host` - (optional) defines the host of the HipChat server. When not defined it fall back on the default host (see bellow) +* `port` - (optional) defines the port of the HipChat server. When not defined it fall back on the default port (see bellow) +* `message_defaults` - (optional) a set of settings to define the default settings of the messages that are sent via this account +* `room` - Some account are bound to a single room (messages that are sent using their associated profiles can only be + sent to a specific room). For those account, this setting defines the room the account is bound to. + +Here's an example settings for HipChat service: + +[source,yaml] +-------------------------------------------------- +watcher.actions.hipchat: + default_account: v1 + account: + account1: + profile: v1 + auth_token: XXXXXXXXX + message_default: + color: yello + message_format: text + account2: + profile: integration + auth_token: YYYYYYYYY + room: mission-control + message_default: + color: red + message_format: text +-------------------------------------------------- + +[[hipchat-api-v1]] +===== `v1` Account + +WARNING: This account uses a deprecated API and is expected to be removed by HipChat in the future. + +The `v1` API was the first API HipChat ever exposed and therefore the most commonly used one. It is also the simplest +one to set up. To create the `v1` API token, please follow the instructions listed on https://www.hipchat.com/docs/api. + +NOTE: User private messages are not supported by this API. If private messages is what you are after, please + consider the <<`user`>> API instead. + +Once the an API is created, add the following settings in `elasticsearch.yml` file: + +The following table lists the available fields when setting up the `hipchat` action for the `v1` API: + +[[hipchat-api-v1-action-attributes]] +.v1 API Action Attributes +[options="header"] +|====== +| Name |Required | Default | Description +| `from` | no | the watch id | The name that will appear as the sender of the notification +| `room` | yes | - | The room/s that the notification should go to. Accepts a string value or an array of string values. +| `message` | yes | - | The content of the notification message (size is limited by HipChat to 1000 characters) +| `message_format` | no | html* | The format of the message. Possible options are: `text` or `html` +| `color` | no | yellow* | The background color of the notification in the room +| `notify` | no | false | Indicates whether people in the room should be actively notified +|====== + +Here is an example for how it looks like as part of the action definition: + +[source,json] +-------------------------------------------------- +"actions" : { + "notify-hipchat" : { + "transform" : { ... }, + "throttle_period" : "5m", + "hipchat" : { + "account" : "v1-account", + "message" : { + "from" : "watcher", + "room" : [ "server-status", "infra-team" ], + "message" : "Encountered {{ctx.payload.hits.total}} errors in the last 5 minutes (facepalm)", + "message_format" : "text", + "color" : "red", + "notify" : true + } + } + } +} +-------------------------------------------------- + + +[[hipchat-api-integration]] +===== `integration` Accounts + +This profiles uses HipChat https://www.hipchat.com/docs/apiv2/addons[Integrations]. More specifically, +it uses a built-in integration in HipChat that enables external systems to send notifications to a +specific room. To create the `integration` API token: + +* For HipChat.com, please follow the "Build your own integration" https://www.hipchat.com/docs/apiv2[instructions] +* For HipChat Server, please follow the "Build your own integration" https://confluence.atlassian.com/hc/administering-hipchat-server/integrations-with-hipchat-server[instructions] + +In both cases, the api token can be copied from the listed example (marked in red bellow) + +image:images/hipchat-integration-example.png[] + +NOTE: This API is the most limited APIs of the three as it only supports sending notifications to a single room and + does not support user private messages. If you are looking for multi-room notifications, please consider either + the <> or <> APIs. Only the latter supports user private + messages. + +When creating an account with the `integration` profile, you must configure the `room` setting as part +of the account setting. + +The following table lists the available fields when setting up the `hipchat` action an `integration` account: + +[[hipchat-api-integration-action-attributes]] +.v1 API Action Attributes +[options="header"] +|====== +| Name |Required | Default | Description +| `message` | yes | - | The content of the notification message (size is limited by HipChat to 1000 characters) +| `message_format` | no | html* | The format of the message. Possible options are: `text` or `html` +| `color` | no | yellow* | The background color of the notification in the room +| `notify` | no | false | Indicates whether people in the room should be actively notified +|====== + +Here is an example for how it looks like as part of the action definition: + +[source,json] +-------------------------------------------------- +"actions" : { + "notify-hipchat" : { + "transform" : { ... }, + "throttle_period" : "5m", + "hipchat" : { + "account" : "integration-account", + "message" : { + "message" : "Encountered {{ctx.payload.hits.total}} errors in the last 5 minutes (facepalm)", + "message_format" : "text", + "color" : "red", + "notify" : true + } + } + } +} +-------------------------------------------------- + +[[hipchat-api-user]] +===== `user` Accounts + +The `user` API is arguably the most flexible API. It is also safe to use as it and is based on HipChat's `v2` API version. +To use this API you will require to add a new HipChat user. With this the user in place, all messages sent via this +account will be sent on this user behalf (make sure you name the user appropriately). After creating the user, you need +to create an API token for it. To create a user token please follow the instructions on HipChat's online documentation. +//// +TODO: could not find a good link for that... we might need to show screenshots of the UI +//// + +While not supported by `v1` and `integration` accounts, the `user` account enables private user notification. + +The following table lists the available fields when setting up the `hipchat` action for the `user` API: + +[[hipchat-api-user-action-attributes]] +.v1 API Action Attributes +[options="header"] +|====== +| Name |Required | Default | Description +| `message` | yes | - | The content of the notification message (size is limited by HipChat to 1000 characters) +| `message_format` | no | html* | The format of the message. Possible options are: `text` or `html` +| `color` | no | yellow* | The background color of the notification in the room +| `notify` | no | false | Indicates whether people in the room should be actively notified +|====== + +Here is an example for how it looks like as part of the action definition: + +[source,json] +-------------------------------------------------- +"actions" : { + "notify-hipchat" : { + "transform" : { ... }, + "throttle_period" : "5m", + "hipchat" : { + "account" : "integration-account", + "message" : { + "room" : [ "mission-control", "devops" ], + "user" : "website-admin@example.com", + "message" : "Encountered {{ctx.payload.hits.total}} errors in the last 5 minutes (facepalm)", + "message_format" : "text", + "color" : "red", + "notify" : true + } + } + } +} +-------------------------------------------------- diff --git a/watcher/src/main/java/org/elasticsearch/watcher/WatcherPlugin.java b/watcher/src/main/java/org/elasticsearch/watcher/WatcherPlugin.java index 67b0a1abd1c..e6b0aa3fd5f 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/WatcherPlugin.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/WatcherPlugin.java @@ -20,6 +20,7 @@ import org.elasticsearch.script.ScriptModule; import org.elasticsearch.shield.authz.AuthorizationModule; import org.elasticsearch.watcher.actions.WatcherActionModule; import org.elasticsearch.watcher.actions.email.service.InternalEmailService; +import org.elasticsearch.watcher.actions.hipchat.service.InternalHipChatService; import org.elasticsearch.watcher.client.WatcherClientModule; import org.elasticsearch.watcher.condition.ConditionModule; import org.elasticsearch.watcher.execution.ExecutionModule; @@ -27,15 +28,7 @@ import org.elasticsearch.watcher.history.HistoryModule; import org.elasticsearch.watcher.input.InputModule; import org.elasticsearch.watcher.license.LicenseModule; import org.elasticsearch.watcher.license.LicenseService; -import org.elasticsearch.watcher.rest.action.RestAckWatchAction; -import org.elasticsearch.watcher.rest.action.RestDeleteWatchAction; -import org.elasticsearch.watcher.rest.action.RestExecuteWatchAction; -import org.elasticsearch.watcher.rest.action.RestGetWatchAction; -import org.elasticsearch.watcher.rest.action.RestHijackOperationAction; -import org.elasticsearch.watcher.rest.action.RestPutWatchAction; -import org.elasticsearch.watcher.rest.action.RestWatchServiceAction; -import org.elasticsearch.watcher.rest.action.RestWatcherInfoAction; -import org.elasticsearch.watcher.rest.action.RestWatcherStatsAction; +import org.elasticsearch.watcher.rest.action.*; import org.elasticsearch.watcher.shield.ShieldIntegration; import org.elasticsearch.watcher.shield.WatcherShieldModule; import org.elasticsearch.watcher.shield.WatcherUserHolder; @@ -142,6 +135,7 @@ public class WatcherPlugin extends Plugin { InitializingService.class, LicenseService.class, InternalEmailService.class, + InternalHipChatService.class, HttpClient.class, WatcherSettingsValidation.class); } diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/Action.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/Action.java index ee051d60ec3..d1dde257771 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/actions/Action.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/Action.java @@ -25,6 +25,7 @@ public interface Action extends ToXContent { public enum Status implements ToXContent { SUCCESS, FAILURE, + PARTIAL_FAILURE, THROTTLED, SIMULATED; @@ -95,7 +96,6 @@ public interface Action extends ToXContent { } interface Field { - ParseField STATUS = new ParseField("status"); ParseField REASON = new ParseField("reason"); } } diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java index e38762ee32a..e1b5eb2755f 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/ActionBuilders.java @@ -7,6 +7,7 @@ package org.elasticsearch.watcher.actions; import org.elasticsearch.watcher.actions.email.EmailAction; import org.elasticsearch.watcher.actions.email.service.EmailTemplate; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; import org.elasticsearch.watcher.actions.index.IndexAction; import org.elasticsearch.watcher.actions.logging.LoggingAction; import org.elasticsearch.watcher.actions.webhook.WebhookAction; @@ -53,4 +54,27 @@ public final class ActionBuilders { return LoggingAction.builder(text); } + public static HipChatAction.Builder hipchatAction(String message) { + return hipchatAction(Template.inline(message)); + } + + public static HipChatAction.Builder hipchatAction(String account, String body) { + return hipchatAction(account, Template.inline(body)); + } + + public static HipChatAction.Builder hipchatAction(Template.Builder body) { + return hipchatAction(body.build()); + } + + public static HipChatAction.Builder hipchatAction(String account, Template.Builder body) { + return hipchatAction(account, body.build()); + } + + public static HipChatAction.Builder hipchatAction(Template body) { + return hipchatAction(null, body); + } + + public static HipChatAction.Builder hipchatAction(String account, Template body) { + return HipChatAction.builder(account, body); + } } diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/WatcherActionModule.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/WatcherActionModule.java index d2647dbc0ab..4733fb2a819 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/actions/WatcherActionModule.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/WatcherActionModule.java @@ -12,6 +12,10 @@ import org.elasticsearch.watcher.actions.email.EmailActionFactory; import org.elasticsearch.watcher.actions.email.service.EmailService; import org.elasticsearch.watcher.actions.email.service.HtmlSanitizer; import org.elasticsearch.watcher.actions.email.service.InternalEmailService; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; +import org.elasticsearch.watcher.actions.hipchat.HipChatActionFactory; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatService; +import org.elasticsearch.watcher.actions.hipchat.service.InternalHipChatService; import org.elasticsearch.watcher.actions.index.IndexAction; import org.elasticsearch.watcher.actions.index.IndexActionFactory; import org.elasticsearch.watcher.actions.logging.LoggingAction; @@ -28,6 +32,14 @@ public class WatcherActionModule extends AbstractModule { private final Map> parsers = new HashMap<>(); + public WatcherActionModule() { + registerAction(EmailAction.TYPE, EmailActionFactory.class); + registerAction(WebhookAction.TYPE, WebhookActionFactory.class); + registerAction(IndexAction.TYPE, IndexActionFactory.class); + registerAction(LoggingAction.TYPE, LoggingActionFactory.class); + registerAction(HipChatAction.TYPE, HipChatActionFactory.class); + } + public void registerAction(String type, Class parserType) { parsers.put(type, parserType); } @@ -36,27 +48,17 @@ public class WatcherActionModule extends AbstractModule { protected void configure() { MapBinder parsersBinder = MapBinder.newMapBinder(binder(), String.class, ActionFactory.class); - - bind(EmailActionFactory.class).asEagerSingleton(); - parsersBinder.addBinding(EmailAction.TYPE).to(EmailActionFactory.class); - - bind(WebhookActionFactory.class).asEagerSingleton(); - parsersBinder.addBinding(WebhookAction.TYPE).to(WebhookActionFactory.class); - - bind(IndexActionFactory.class).asEagerSingleton(); - parsersBinder.addBinding(IndexAction.TYPE).to(IndexActionFactory.class); - - bind(LoggingActionFactory.class).asEagerSingleton(); - parsersBinder.addBinding(LoggingAction.TYPE).to(LoggingActionFactory.class); - for (Map.Entry> entry : parsers.entrySet()) { bind(entry.getValue()).asEagerSingleton(); parsersBinder.addBinding(entry.getKey()).to(entry.getValue()); } bind(ActionRegistry.class).asEagerSingleton(); + bind(HtmlSanitizer.class).asEagerSingleton(); bind(EmailService.class).to(InternalEmailService.class).asEagerSingleton(); + + bind(HipChatService.class).to(InternalHipChatService.class).asEagerSingleton(); } diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/email/service/InternalEmailService.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/email/service/InternalEmailService.java index 2b41648c271..92bf6e1a1b7 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/actions/email/service/InternalEmailService.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/email/service/InternalEmailService.java @@ -79,13 +79,15 @@ public class InternalEmailService extends AbstractLifecycleComponent { + + private final TemplateEngine templateEngine; + private final HipChatService hipchatService; + + public ExecutableHipChatAction(HipChatAction action, ESLogger logger, HipChatService hipchatService, TemplateEngine templateEngine) { + super(action, logger); + this.hipchatService = hipchatService; + this.templateEngine = templateEngine; + } + + @Override + public Action.Result execute(final String actionId, WatchExecutionContext ctx, Payload payload) throws Exception { + + HipChatAccount account = action.account != null ? + hipchatService.getAccount(action.account) : + hipchatService.getDefaultAccount(); + + // lets validate the message again, in case the hipchat service were updated since the + // watch/action were created. + account.validateParsedTemplate(ctx.id().watchId(), actionId, action.message); + + Map model = Variables.createCtxModel(ctx, payload); + HipChatMessage message = account.render(ctx.id().watchId(), actionId, templateEngine, action.message, model); + + if (ctx.simulateAction(actionId)) { + return new HipChatAction.Result.Simulated(message); + } + + SentMessages sentMessages = account.send(message); + return new HipChatAction.Result.Executed(sentMessages); + } + +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatAction.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatAction.java new file mode 100644 index 00000000000..d4b5b375168 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatAction.java @@ -0,0 +1,257 @@ +/* + * 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.watcher.actions.hipchat; + + +import org.elasticsearch.ElasticsearchParseException; +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.watcher.actions.Action; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage; +import org.elasticsearch.watcher.actions.hipchat.service.SentMessages; +import org.elasticsearch.watcher.support.template.Template; + +import javax.annotation.Nullable; +import java.io.IOException; + +/** + * + */ +public class HipChatAction implements Action { + + public static final String TYPE = "hipchat"; + + final @Nullable String account; + final HipChatMessage.Template message; + + public HipChatAction(@Nullable String account, HipChatMessage.Template message) { + this.account = account; + this.message = message; + } + + @Override + public String type() { + return TYPE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HipChatAction that = (HipChatAction) o; + + if (!account.equals(that.account)) return false; + return message.equals(that.message); + } + + @Override + public int hashCode() { + int result = account.hashCode(); + result = 31 * result + message.hashCode(); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (account != null) { + builder.field(Field.ACCOUNT.getPreferredName(), account); + } + builder.field(Field.MESSAGE.getPreferredName(), message); + return builder.endObject(); + } + + public static HipChatAction parse(String watchId, String actionId, XContentParser parser) throws IOException { + String account = null; + HipChatMessage.Template message = 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.MESSAGE)) { + try { + message = HipChatMessage.Template.parse(parser); + } catch (Exception e) { + throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. failed to parse [{}] field", e, TYPE, watchId, actionId, Field.MESSAGE.getPreferredName()); + } + } else { + throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. unexpected token [{}]", TYPE, watchId, actionId, token); + } + } + + if (message == null) { + throw new ElasticsearchParseException("failed to parse [{}] action [{}/{}]. missing required [{}] field", TYPE, watchId, actionId, Field.MESSAGE.getPreferredName()); + } + + return new HipChatAction(account, message); + } + + public static Builder builder(String account, Template body) { + return new Builder(account, body); + } + + public interface Result { + + class Executed extends Action.Result implements Result { + + private final SentMessages sentMessages; + + public Executed(SentMessages sentMessages) { + super(TYPE, status(sentMessages)); + this.sentMessages = sentMessages; + } + + public SentMessages sentMessages() { + return sentMessages; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.field(type, sentMessages, params); + } + + static Status status(SentMessages sentMessages) { + boolean hasSuccesses = false; + boolean hasFailures = false; + for (SentMessages.SentMessage message : sentMessages) { + if (message.successful()) { + hasSuccesses = true; + } else { + hasFailures = true; + } + if (hasFailures && hasSuccesses) { + return Status.PARTIAL_FAILURE; + } + } + return hasFailures ? Status.FAILURE : Status.SUCCESS; + } + } + + class Simulated extends Action.Result implements Result { + + private final HipChatMessage message; + + protected Simulated(HipChatMessage message) { + super(TYPE, Status.SIMULATED); + this.message = message; + } + + public HipChatMessage getMessage() { + return message; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject(type) + .field(Field.MESSAGE.getPreferredName(), message, params) + .endObject(); + } + } + } + + public static class Builder implements Action.Builder { + + final String account; + final HipChatMessage.Template.Builder messageBuilder; + + public Builder(String account, Template body) { + this.account = account; + this.messageBuilder = new HipChatMessage.Template.Builder(body); + } + + public Builder addRooms(org.elasticsearch.watcher.support.template.Template... rooms) { + messageBuilder.addRooms(rooms); + return this; + } + + public Builder addRooms(org.elasticsearch.watcher.support.template.Template.Builder... rooms) { + org.elasticsearch.watcher.support.template.Template[] templates = new org.elasticsearch.watcher.support.template.Template[rooms.length]; + for (int i = 0; i < rooms.length; i++) { + templates[i] = rooms[i].build(); + } + return addRooms(templates); + } + + public Builder addRooms(String... rooms) { + org.elasticsearch.watcher.support.template.Template[] templates = new org.elasticsearch.watcher.support.template.Template[rooms.length]; + for (int i = 0; i < rooms.length; i++) { + templates[i] = Template.inline(rooms[i]).build(); + } + return addRooms(templates); + } + + + public Builder addUsers(org.elasticsearch.watcher.support.template.Template... users) { + messageBuilder.addUsers(users); + return this; + } + + public Builder addUsers(org.elasticsearch.watcher.support.template.Template.Builder... users) { + org.elasticsearch.watcher.support.template.Template[] templates = new org.elasticsearch.watcher.support.template.Template[users.length]; + for (int i = 0; i < users.length; i++) { + templates[i] = users[i].build(); + } + return addUsers(templates); + } + + public Builder addUsers(String... users) { + org.elasticsearch.watcher.support.template.Template[] templates = new org.elasticsearch.watcher.support.template.Template[users.length]; + for (int i = 0; i < users.length; i++) { + templates[i] = Template.inline(users[i]).build(); + } + return addUsers(templates); + } + + public Builder setFrom(String from) { + messageBuilder.setFrom(from); + return this; + } + + public Builder setFormat(HipChatMessage.Format format) { + messageBuilder.setFormat(format); + return this; + } + + public Builder setColor(org.elasticsearch.watcher.support.template.Template color) { + messageBuilder.setColor(color); + return this; + } + + public Builder setColor(org.elasticsearch.watcher.support.template.Template.Builder color) { + return setColor(color.build()); + } + + public Builder setColor(HipChatMessage.Color color) { + return setColor(color.asTemplate()); + } + + public Builder setNotify(boolean notify) { + messageBuilder.setNotify(notify); + return this; + } + + @Override + public HipChatAction build() { + return new HipChatAction(account, messageBuilder.build()); + } + } + + public interface Field { + ParseField ACCOUNT = new ParseField("account"); + ParseField MESSAGE = new ParseField("message"); + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactory.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactory.java new file mode 100644 index 00000000000..b2fb240ec9e --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactory.java @@ -0,0 +1,56 @@ +/* + * 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.watcher.actions.hipchat; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.watcher.actions.ActionFactory; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatAccount; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatService; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import java.io.IOException; + +/** + * + */ +public class HipChatActionFactory extends ActionFactory { + + private final TemplateEngine templateEngine; + private final HipChatService hipchatService; + + @Inject + public HipChatActionFactory(Settings settings, TemplateEngine templateEngine, HipChatService hipchatService) { + super(Loggers.getLogger(ExecutableHipChatAction.class, settings)); + this.templateEngine = templateEngine; + this.hipchatService = hipchatService; + } + + @Override + public String type() { + return HipChatAction.TYPE; + } + + @Override + public HipChatAction parseAction(String watchId, String actionId, XContentParser parser) throws IOException { + HipChatAction action = HipChatAction.parse(watchId, actionId, parser); + HipChatAccount account = hipchatService.getAccount(action.account); + if (account == null) { + throw new ElasticsearchParseException("could not parse [hipchat] action [{}/{}]. unknown hipchat account [{}]", watchId, account, action.account); + } + account.validateParsedTemplate(watchId, actionId, action.message); + return action; + } + + @Override + public ExecutableHipChatAction createExecutable(HipChatAction action) { + return new ExecutableHipChatAction(action, actionLogger, hipchatService, templateEngine); + } + +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccount.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccount.java new file mode 100644 index 00000000000..7f50a82d0e8 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccount.java @@ -0,0 +1,120 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.watcher.support.http.HttpClient; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +/** + * + */ +public abstract class HipChatAccount { + + public static final String AUTH_TOKEN_SETTING = "auth_token"; + public static final String ROOM_SETTING = HipChatMessage.Field.ROOM.getPreferredName(); + public static final String DEFAULT_ROOM_SETTING = "message_defaults." + HipChatMessage.Field.ROOM.getPreferredName(); + public static final String DEFAULT_USER_SETTING = "message_defaults." + HipChatMessage.Field.USER.getPreferredName(); + public static final String DEFAULT_FROM_SETTING = "message_defaults." + HipChatMessage.Field.FROM.getPreferredName(); + public static final String DEFAULT_FORMAT_SETTING = "message_defaults." + HipChatMessage.Field.FORMAT.getPreferredName(); + public static final String DEFAULT_COLOR_SETTING = "message_defaults." + HipChatMessage.Field.COLOR.getPreferredName(); + public static final String DEFAULT_NOTIFY_SETTING = "message_defaults." + HipChatMessage.Field.NOTIFY.getPreferredName(); + + protected final ESLogger logger; + protected final String name; + protected final Profile profile; + protected final HipChatServer server; + protected final HttpClient httpClient; + protected final String authToken; + + protected HipChatAccount(String name, Profile profile, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + this.name = name; + this.profile = profile; + this.server = new HipChatServer(settings, defaultServer); + this.httpClient = httpClient; + this.authToken = settings.get(AUTH_TOKEN_SETTING); + if (this.authToken == null || this.authToken.length() == 0) { + throw new SettingsException("hipchat account [" + name + "] missing required [" + AUTH_TOKEN_SETTING + "] setting"); + } + this.logger = logger; + } + + public abstract String type(); + + public abstract void validateParsedTemplate(String watchId, String actionId, HipChatMessage.Template message) throws SettingsException; + + public abstract HipChatMessage render(String watchId, String actionId, TemplateEngine engine, HipChatMessage.Template template, Map model); + + public abstract SentMessages send(HipChatMessage message); + + enum Profile implements ToXContent { + + V1() { + @Override + HipChatAccount createAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + return new V1Account(name, settings, defaultServer, httpClient, logger); + } + }, + INTEGRATION() { + @Override + HipChatAccount createAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + return new IntegrationAccount(name, settings, defaultServer, httpClient, logger); + } + }, + USER() { + @Override + HipChatAccount createAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + return new UserAccount(name, settings, defaultServer, httpClient, logger); + } + }; + + abstract HipChatAccount createAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger); + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(name().toLowerCase(Locale.ROOT)); + } + + public String value() { + return name().toLowerCase(Locale.ROOT); + } + + public static Profile parse(XContentParser parser) throws IOException { + return Profile.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } + + public static Profile resolve(String value, Profile defaultValue) { + if (value == null) { + return defaultValue; + } + return Profile.valueOf(value.toUpperCase(Locale.ROOT)); + } + + public static Profile resolve(Settings settings, String setting, Profile defaultValue) { + return resolve(settings.get(setting), defaultValue); + } + + public static boolean validate(String value) { + try { + Profile.valueOf(value.toUpperCase(Locale.ROOT)); + return true; + } catch (IllegalArgumentException ilae) { + return false; + } + } + + } + +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccounts.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccounts.java new file mode 100644 index 00000000000..2a73524c35a --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccounts.java @@ -0,0 +1,72 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatAccount.Profile; +import org.elasticsearch.watcher.support.http.HttpClient; + +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class HipChatAccounts { + + private final Map accounts; + private final String defaultAccountName; + + public HipChatAccounts(Settings settings, HttpClient httpClient, ESLogger logger) { + HipChatServer defaultServer = new HipChatServer(settings); + Settings accountsSettings = settings.getAsSettings("account"); + accounts = new HashMap<>(); + for (String name : accountsSettings.names()) { + Settings accountSettings = accountsSettings.getAsSettings(name); + Profile profile = Profile.resolve(accountSettings, "profile", null); + if (profile == null) { + throw new SettingsException("missing [profile] setting for hipchat account [" + name + "]"); + } + HipChatAccount account = profile.createAccount(name, accountSettings, defaultServer, httpClient, logger); + accounts.put(name, account); + } + + String defaultAccountName = settings.get("default_account"); + if (defaultAccountName == null) { + if (accounts.isEmpty()) { + this.defaultAccountName = null; + } else { + HipChatAccount account = accounts.values().iterator().next(); + logger.info("default hipchat account set to [{}]", account.name); + this.defaultAccountName = account.name; + } + } else if (!accounts.containsKey(defaultAccountName)) { + throw new SettingsException("could not find default hipchat account [" + defaultAccountName + "]"); + } else { + this.defaultAccountName = defaultAccountName; + } + } + + /** + * Returns the account associated with the given name. If there is not such account, {@code null} is returned. + * If the given name is {@code null}, the default account will be returned. + * + * @param name The name of the requested account + * @return The account associated with the given name, or {@code null} when requested an unkonwn account. + * @throws IllegalStateException if the name is null and the default account is null. + */ + public HipChatAccount account(String name) throws IllegalStateException { + if (name == null) { + if (defaultAccountName == null) { + throw new IllegalStateException("cannot find default hipchat account as no accounts have been configured"); + } + name = defaultAccountName; + } + return accounts.get(name); + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessage.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessage.java new file mode 100644 index 00000000000..cb73a43b5cb --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessage.java @@ -0,0 +1,489 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParseFieldMatcher; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.*; + +/** + * + */ +public class HipChatMessage implements ToXContent { + + final String body; + final @Nullable String[] rooms; + final @Nullable String[] users; + final @Nullable String from; + final @Nullable Format format; + final @Nullable Color color; + final @Nullable Boolean notify; + + public HipChatMessage(String body, String[] rooms, String[] users, String from, Format format, Color color, Boolean notify) { + this.body = body; + this.rooms = rooms; + this.users = users; + this.from = from; + this.format = format; + this.color = color; + this.notify = notify; + } + + public String getBody() { + return body; + } + + public String[] getRooms() { + return rooms; + } + + @Nullable + public String[] getUsers() { + return users; + } + + @Nullable + public String getFrom() { + return from; + } + + @Nullable + public Format getFormat() { + return format; + } + + @Nullable + public Color getColor() { + return color; + } + + @Nullable + public Boolean getNotify() { + return notify; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HipChatMessage that = (HipChatMessage) o; + + if (!body.equals(that.body)) return false; + if (!Arrays.equals(rooms, that.rooms)) return false; + if (!Arrays.equals(users, that.users)) return false; + if (from != null ? !from.equals(that.from) : that.from != null) return false; + if (format != that.format) return false; + if (color != that.color) return false; + return !(notify != null ? !notify.equals(that.notify) : that.notify != null); + } + + @Override + public int hashCode() { + int result = body.hashCode(); + result = 31 * result + (rooms != null ? Arrays.hashCode(rooms) : 0); + result = 31 * result + (users != null ? Arrays.hashCode(users) : 0); + result = 31 * result + (from != null ? from.hashCode() : 0); + result = 31 * result + (format != null ? format.hashCode() : 0); + result = 31 * result + (color != null ? color.hashCode() : 0); + result = 31 * result + (notify != null ? notify.hashCode() : 0); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return toXContent(builder, params, true); + } + + public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean includeTargets) throws IOException { + builder.startObject(); + if (from != null) { + builder.field(Field.FROM.getPreferredName(), from); + } + if (includeTargets) { + if (rooms != null && rooms.length > 0) { + builder.array(Field.ROOM.getPreferredName(), rooms); + } + if (users != null && users.length > 0) { + builder.array(Field.USER.getPreferredName(), users); + } + } + builder.field(Field.BODY.getPreferredName(), body); + if (format != null) { + builder.field(Field.FORMAT.getPreferredName(), format, params); + } + if (color != null) { + builder.field(Field.COLOR.getPreferredName(), color, params); + } + if (notify != null) { + builder.field(Field.NOTIFY.getPreferredName(), notify); + } + return builder.endObject(); + } + + public static class Template implements ToXContent { + + final org.elasticsearch.watcher.support.template.Template body; + final @Nullable org.elasticsearch.watcher.support.template.Template[] rooms; + final @Nullable org.elasticsearch.watcher.support.template.Template[] users; + final @Nullable String from; + final @Nullable Format format; + final @Nullable org.elasticsearch.watcher.support.template.Template color; + final @Nullable Boolean notify; + + public Template(org.elasticsearch.watcher.support.template.Template body, + org.elasticsearch.watcher.support.template.Template[] rooms, + org.elasticsearch.watcher.support.template.Template[] users, + String from, + Format format, + org.elasticsearch.watcher.support.template.Template color, + Boolean notify) { + this.rooms = rooms; + this.users = users; + this.body = body; + this.from = from; + this.format = format; + this.color = color; + this.notify = notify; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Template template = (Template) o; + + if (!body.equals(template.body)) return false; + if (!Arrays.equals(rooms, template.rooms)) return false; + if (!Arrays.equals(users, template.users)) return false; + if (from != null ? !from.equals(template.from) : template.from != null) return false; + if (format != template.format) return false; + if (color != null ? !color.equals(template.color) : template.color != null) return false; + return !(notify != null ? !notify.equals(template.notify) : template.notify != null); + } + + @Override + public int hashCode() { + int result = body.hashCode(); + result = 31 * result + (rooms != null ? Arrays.hashCode(rooms) : 0); + result = 31 * result + (users != null ? Arrays.hashCode(users) : 0); + result = 31 * result + (from != null ? from.hashCode() : 0); + result = 31 * result + (format != null ? format.hashCode() : 0); + result = 31 * result + (color != null ? color.hashCode() : 0); + result = 31 * result + (notify != null ? notify.hashCode() : 0); + return result; + } + + public HipChatMessage render(TemplateEngine engine, Map model) { + String body = engine.render(this.body, model); + String[] rooms = null; + if (this.rooms != null) { + rooms = new String[this.rooms.length]; + for (int i = 0; i < this.rooms.length; i++) { + rooms[i] = engine.render(this.rooms[i], model); + } + } + String[] users = null; + if (this.users != null) { + users = new String[this.users.length]; + for (int i = 0; i < this.users.length; i++) { + users[i] = engine.render(this.users[i], model); + } + } + Color color = this.color == null ? null : Color.resolve(engine.render(this.color, model), null); + return new HipChatMessage(body, rooms, users, from, format, color, notify); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (from != null) { + builder.field(Field.FROM.getPreferredName(), from); + } + if (rooms != null && rooms.length > 0) { + builder.startArray(Field.ROOM.getPreferredName()); + for (org.elasticsearch.watcher.support.template.Template room : rooms) { + room.toXContent(builder, params); + } + builder.endArray(); + } + if (users != null && users.length > 0) { + builder.startArray(Field.USER.getPreferredName()); + for (org.elasticsearch.watcher.support.template.Template user : users) { + user.toXContent(builder, params); + } + builder.endArray(); + } + builder.field(Field.BODY.getPreferredName(), body, params); + if (format != null) { + builder.field(Field.FORMAT.getPreferredName(), format, params); + } + if (color != null) { + builder.field(Field.COLOR.getPreferredName(), color, params); + } + if (notify != null) { + builder.field(Field.NOTIFY.getPreferredName(), notify); + } + return builder.endObject(); + } + + public static Template parse(XContentParser parser) throws IOException { + org.elasticsearch.watcher.support.template.Template body = null; + org.elasticsearch.watcher.support.template.Template[] rooms = null; + org.elasticsearch.watcher.support.template.Template[] users = null; + String from = null; + org.elasticsearch.watcher.support.template.Template color = null; + Boolean notify = null; + HipChatMessage.Format messageFormat = 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.FROM)) { + from = parser.text(); + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.ROOM)) { + List templates = new ArrayList<>(); + if (token == XContentParser.Token.START_ARRAY) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + try { + templates.add(org.elasticsearch.watcher.support.template.Template.parse(parser)); + } catch (ElasticsearchParseException epe) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", epe, Field.ROOM.getPreferredName()); + } + } + } else { + try { + templates.add(org.elasticsearch.watcher.support.template.Template.parse(parser)); + } catch (ElasticsearchParseException epe) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", epe, Field.ROOM.getPreferredName()); + } + } + rooms = templates.toArray(new org.elasticsearch.watcher.support.template.Template[templates.size()]); + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.USER)) { + List templates = new ArrayList<>(); + if (token == XContentParser.Token.START_ARRAY) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + try { + templates.add(org.elasticsearch.watcher.support.template.Template.parse(parser)); + } catch (ElasticsearchParseException epe) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", epe, Field.USER.getPreferredName()); + } + } + } else { + try { + templates.add(org.elasticsearch.watcher.support.template.Template.parse(parser)); + } catch (ElasticsearchParseException epe) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", epe, Field.USER.getPreferredName()); + } + } + users = templates.toArray(new org.elasticsearch.watcher.support.template.Template[templates.size()]); + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.COLOR)) { + try { + color = org.elasticsearch.watcher.support.template.Template.parse(parser); + } catch (ElasticsearchParseException | IllegalArgumentException e) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", e, Field.COLOR.getPreferredName()); + } + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.NOTIFY)) { + if (token == XContentParser.Token.VALUE_BOOLEAN) { + notify = parser.booleanValue(); + } else { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field, expected a boolean value but found [{}]", Field.NOTIFY.getPreferredName(), token); + } + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.BODY)) { + try { + body = org.elasticsearch.watcher.support.template.Template.parse(parser); + } catch (ElasticsearchParseException pe) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", pe, Field.BODY.getPreferredName()); + } + } else if (ParseFieldMatcher.STRICT.match(currentFieldName, Field.FORMAT)) { + try { + messageFormat = HipChatMessage.Format.parse(parser); + } catch (IllegalArgumentException ilae) { + throw new ElasticsearchParseException("failed to parse hipchat message. failed to parse [{}] field", ilae, Field.FORMAT.getPreferredName()); + } + } else { + throw new ElasticsearchParseException("failed to parse hipchat message. unexpected token [{}]", token); + } + } + + if (body == null) { + throw new ElasticsearchParseException("failed to parse hipchat message. missing required [{}] field", Field.BODY.getPreferredName()); + } + + return new HipChatMessage.Template(body, rooms, users, from, messageFormat, color, notify); + } + + public static class Builder { + + final org.elasticsearch.watcher.support.template.Template body; + final List rooms = new ArrayList<>(); + final List users = new ArrayList<>(); + @Nullable String from; + @Nullable Format format; + @Nullable org.elasticsearch.watcher.support.template.Template color; + @Nullable Boolean notify; + + public Builder(org.elasticsearch.watcher.support.template.Template body) { + this.body = body; + } + + public Builder addRooms(org.elasticsearch.watcher.support.template.Template... rooms) { + this.rooms.addAll(Arrays.asList(rooms)); + return this; + } + + public Builder addUsers(org.elasticsearch.watcher.support.template.Template... users) { + this.users.addAll(Arrays.asList(users)); + return this; + } + + public Builder setFrom(String from) { + this.from = from; + return this; + } + + public Builder setFormat(Format format) { + this.format = format; + return this; + } + + public Builder setColor(org.elasticsearch.watcher.support.template.Template color) { + this.color = color; + return this; + } + + public Builder setNotify(boolean notify) { + this.notify = notify; + return this; + } + + public Template build() { + return new Template( + body, + rooms.isEmpty() ? null : rooms.toArray(new org.elasticsearch.watcher.support.template.Template[rooms.size()]), + users.isEmpty() ? null : users.toArray(new org.elasticsearch.watcher.support.template.Template[users.size()]), + from, + format, + color, + notify); + } + } + } + + + public enum Color implements ToXContent { + YELLOW, GREEN, RED, PURPLE, GRAY, RANDOM; + + private final org.elasticsearch.watcher.support.template.Template template = org.elasticsearch.watcher.support.template.Template.inline(name()).build(); + + public org.elasticsearch.watcher.support.template.Template asTemplate() { + return template; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(name().toLowerCase(Locale.ROOT)); + } + + public String value() { + return name().toLowerCase(Locale.ROOT); + } + + public static Color parse(XContentParser parser) throws IOException { + return Color.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } + + public static Color resolve(String value, Color defaultValue) { + if (value == null) { + return defaultValue; + } + return Color.valueOf(value.toUpperCase(Locale.ROOT)); + } + + public static Color resolve(Settings settings, String setting, Color defaultValue) { + return resolve(settings.get(setting), defaultValue); + } + + public static boolean validate(String value) { + try { + Color.valueOf(value.toUpperCase(Locale.ROOT)); + return true; + } catch (IllegalArgumentException ilae) { + return false; + } + } + } + + /** + * + */ + public enum Format implements ToXContent { + + TEXT, + HTML; + + private final org.elasticsearch.watcher.support.template.Template template = org.elasticsearch.watcher.support.template.Template.inline(name()).build(); + + public org.elasticsearch.watcher.support.template.Template asTemplate() { + return template; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(name().toLowerCase(Locale.ROOT)); + } + + public String value() { + return name().toLowerCase(Locale.ROOT); + } + + public static Format parse(XContentParser parser) throws IOException { + return Format.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } + + public static Format resolve(String value, Format defaultValue) { + if (value == null) { + return defaultValue; + } + return Format.valueOf(value.toUpperCase(Locale.ROOT)); + } + + public static Format resolve(Settings settings, String setting, Format defaultValue) { + return resolve(settings.get(setting), defaultValue); + } + + public static boolean validate(String value) { + try { + Format.valueOf(value.toUpperCase(Locale.ROOT)); + return true; + } catch (IllegalArgumentException ilae) { + return false; + } + } + } + + public interface Field { + ParseField ROOM = new ParseField("room"); + ParseField USER = new ParseField("user"); + ParseField BODY = new ParseField("body"); + ParseField FROM = new ParseField("from"); + ParseField COLOR = new ParseField("color"); + ParseField NOTIFY = new ParseField("notify"); + ParseField FORMAT = new ParseField("format"); + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServer.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServer.java new file mode 100644 index 00000000000..7ab2dd47a55 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServer.java @@ -0,0 +1,59 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.watcher.support.http.HttpRequest; + +/** + * + */ +public class HipChatServer { + + public static final String HOST_SETTING = "host"; + public static final String PORT_SETTING = "port"; + + public static final HipChatServer DEFAULT = new HipChatServer("api.hipchat.com", 443, null); + + private final String host; + private final int port; + private final HipChatServer fallback; + + public HipChatServer(Settings settings) { + this(settings, DEFAULT); + } + + public HipChatServer(Settings settings, HipChatServer fallback) { + this(settings.get(HOST_SETTING, null), settings.getAsInt(PORT_SETTING, -1), fallback); + } + + public HipChatServer(String host, int port, HipChatServer fallback) { + this.host = host; + this.port = port; + this.fallback = fallback; + } + + public String host() { + return host != null ? host : fallback.host(); + } + + public int port() { + return port > 0 ? port : fallback.port(); + } + + public HipChatServer fallback() { + return fallback != null ? fallback : DEFAULT; + } + + public HipChatServer rebuild(Settings settings, HipChatServer fallback) { + return new HipChatServer(settings.get(HOST_SETTING, host), settings.getAsInt(PORT_SETTING, port), fallback); + } + + public synchronized HttpRequest.Builder httpRequest() { + return HttpRequest.builder(host(), port()); + } + +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatService.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatService.java new file mode 100644 index 00000000000..8f3b5ea86ec --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatService.java @@ -0,0 +1,24 @@ +/* + * 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.watcher.actions.hipchat.service; + +/** + * + */ +public interface HipChatService { + + /** + * @return The default hipchat account. + */ + HipChatAccount getDefaultAccount(); + + /** + * @return The account identified by the given name. If the given name is {@code null} the default + * account will be returned. + */ + HipChatAccount getAccount(String accountName); + +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccount.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccount.java new file mode 100644 index 00000000000..a3a88cef7e3 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccount.java @@ -0,0 +1,130 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Color; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Format; +import org.elasticsearch.watcher.support.http.*; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class IntegrationAccount extends HipChatAccount { + + public static final String TYPE = "integration"; + + final String room; + final Defaults defaults; + + public IntegrationAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + super(name, Profile.INTEGRATION, settings, defaultServer, httpClient, logger); + String[] rooms = settings.getAsArray(ROOM_SETTING, null); + if (rooms == null || rooms.length == 0) { + throw new SettingsException("invalid hipchat account [" + name + "]. missing required [" + ROOM_SETTING + "] setting for [" + TYPE + "] account profile"); + } + if (rooms.length > 1) { + throw new SettingsException("invalid hipchat account [" + name + "]. [" + ROOM_SETTING + "] setting for [" + TYPE + "] account must only be set with a single value"); + } + this.room = rooms[0]; + defaults = new Defaults(settings); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public void validateParsedTemplate(String watchId, String actionId, HipChatMessage.Template template) throws SettingsException { + if (template.rooms != null) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "] action. [" + name + "] hipchat account doesn't support custom rooms"); + } + if (template.users != null) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "] action. [" + name + "] hipchat account doesn't support user private messages"); + } + if (template.from != null) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "] action. [" + name + "] hipchat account doesn't support custom `from` fields"); + } + } + + @Override + public HipChatMessage render(String watchId, String actionId, TemplateEngine engine, HipChatMessage.Template template, Map model) { + String message = engine.render(template.body, model); + Color color = template.color != null ? Color.resolve(engine.render(template.color, model), defaults.color) : defaults.color; + Boolean notify = template.notify != null ? template.notify : defaults.notify; + Format messageFormat = template.format != null ? template.format : defaults.format; + return new HipChatMessage(message, null, null, null, messageFormat, color, notify); + } + + @Override + public SentMessages send(HipChatMessage message) { + List sentMessages = new ArrayList<>(); + HttpRequest request = buildRoomRequest(room, message); + try { + HttpResponse response = httpClient.execute(request); + sentMessages.add(SentMessages.SentMessage.responded(room, SentMessages.SentMessage.TargetType.ROOM, message, request, response)); + } catch (IOException e) { + logger.error("failed to execute hipchat api http request", e); + sentMessages.add(SentMessages.SentMessage.error(room, SentMessages.SentMessage.TargetType.ROOM, message, ExceptionsHelper.detailedMessage(e))); + } + return new SentMessages(name, sentMessages); + } + + public HttpRequest buildRoomRequest(String room, final HipChatMessage message) { + return server.httpRequest() + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/room/" + room + "/notification") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer " + authToken) + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder xbuilder, Params params) throws IOException { + xbuilder.field("message", message.body); + if (message.format != null) { + xbuilder.field("message_format", message.format.value()); + } + if (message.notify != null) { + xbuilder.field("notify", message.notify); + } + if (message.color != null) { + xbuilder.field("color", String.valueOf(message.color.value())); + } + return xbuilder; + } + })) + .build(); + } + + static class Defaults { + + final @Nullable Format format; + final @Nullable Color color; + final @Nullable Boolean notify; + + public Defaults(Settings settings) { + this.format = Format.resolve(settings, DEFAULT_FORMAT_SETTING, null); + this.color = Color.resolve(settings, DEFAULT_COLOR_SETTING, null); + this.notify = settings.getAsBoolean(DEFAULT_NOTIFY_SETTING, null); + } + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatService.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatService.java new file mode 100644 index 00000000000..31b6cd66a58 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatService.java @@ -0,0 +1,76 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.settings.NodeSettingsService; +import org.elasticsearch.watcher.shield.WatcherSettingsFilter; +import org.elasticsearch.watcher.support.http.HttpClient; + +/** + * + */ +public class InternalHipChatService extends AbstractLifecycleComponent implements HipChatService { + + private final HttpClient httpClient; + private volatile HipChatAccounts accounts; + + @Inject + public InternalHipChatService(Settings settings, HttpClient httpClient, NodeSettingsService nodeSettingsService, WatcherSettingsFilter settingsFilter) { + super(settings); + this.httpClient = httpClient; + nodeSettingsService.addListener(new NodeSettingsService.Listener() { + @Override + public void onRefreshSettings(Settings settings) { + reset(settings); + } + }); + settingsFilter.filterOut("watcher.actions.hipchat.service.account.*.auth_token"); + } + + @Override + protected void doStart() { + reset(settings); + } + + @Override + protected void doStop() { + } + + @Override + protected void doClose() { + } + + @Override + public HipChatAccount getDefaultAccount() { + return accounts.account(null); + } + + @Override + public HipChatAccount getAccount(String name) { + return accounts.account(name); + } + + void reset(Settings nodeSettings) { + Settings.Builder builder = Settings.builder(); + String prefix = "watcher.actions.hipchat.service"; + for (String setting : settings.getAsMap().keySet()) { + if (setting.startsWith(prefix)) { + builder.put(setting.substring(prefix.length()+1), settings.get(setting)); + } + } + if (nodeSettings != settings) { // if it's the same settings, no point in re-applying it + for (String setting : nodeSettings.getAsMap().keySet()) { + if (setting.startsWith(prefix)) { + builder.put(setting.substring(prefix.length() + 1), nodeSettings.get(setting)); + } + } + } + accounts = new HipChatAccounts(builder.build(), httpClient, logger); + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/SentMessages.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/SentMessages.java new file mode 100644 index 00000000000..45b64b02447 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/SentMessages.java @@ -0,0 +1,152 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentBuilderString; +import org.elasticsearch.watcher.support.http.HttpRequest; +import org.elasticsearch.watcher.support.http.HttpResponse; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +/** + * + */ +public class SentMessages implements ToXContent, Iterable { + + private String accountName; + private List messages; + + public SentMessages(String accountName, List messages) { + this.accountName = accountName; + this.messages = messages; + } + + public String getAccountName() { + return accountName; + } + + @Override + public Iterator iterator() { + return messages.iterator(); + } + + public int count() { + return messages.size(); + } + + public List asList() { + return Collections.unmodifiableList(messages); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Field.ACCOUNT, accountName); + builder.startArray(Field.SENT_MESSAGES); + for (SentMessage message : messages) { + message.toXContent(builder, params); + } + builder.endArray(); + return builder.endObject(); + } + + public static class SentMessage implements ToXContent { + + public enum TargetType { + ROOM, USER; + + final XContentBuilderString fieldName = new XContentBuilderString(name().toLowerCase(Locale.ROOT)); + } + + final String targetName; + final TargetType targetType; + final HipChatMessage message; + final @Nullable HttpRequest request; + final @Nullable HttpResponse response; + final @Nullable String failureReason; + + public static SentMessage responded(String targetName, TargetType targetType, HipChatMessage message, HttpRequest request, HttpResponse response) { + String failureReason = resolveFailureReason(response); + return new SentMessage(targetName, targetType, message, request, response, failureReason); + } + + public static SentMessage error(String targetName, TargetType targetType, HipChatMessage message, String reason) { + return new SentMessage(targetName, targetType, message, null, null, reason); + } + + private SentMessage(String targetName, TargetType targetType, HipChatMessage message, HttpRequest request, HttpResponse response, String failureReason) { + this.targetName = targetName; + this.targetType = targetType; + this.message = message; + this.request = request; + this.response = response; + this.failureReason = failureReason; + } + + public boolean successful() { + return failureReason == null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (failureReason != null) { + builder.field(Field.STATUS, "failure"); + builder.field(Field.REASON, failureReason); + if (request != null) { + builder.field(Field.REQUEST); + request.toXContent(builder, params); + } + if (response != null) { + builder.field(Field.RESPONSE); + response.toXContent(builder, params); + } + } else { + builder.field(Field.STATUS, "success"); + } + builder.field(targetType.fieldName, targetName); + builder.field(Field.MESSAGE); + message.toXContent(builder, params, false); + return builder.endObject(); + } + + private static String resolveFailureReason(HttpResponse response) { + int status = response.status(); + if (status < 300) { + return null; + } + switch (status) { + case 400: return "Bad Request"; + case 401: return "Unauthorized. The provided authentication token is invalid."; + case 403: return "Forbidden. The account doesn't have permission to send this message."; + case 404: // Not Found + case 405: // Method Not Allowed + case 406: return "The account used invalid HipChat APIs"; // Not Acceptable + case 503: + case 500: return "HipChat Server Error."; + default: + return "Unknown Error"; + } + } + } + + interface Field { + XContentBuilderString ACCOUNT = new XContentBuilderString("account"); + XContentBuilderString SENT_MESSAGES = new XContentBuilderString("sent_messages"); + XContentBuilderString STATUS = new XContentBuilderString("status"); + XContentBuilderString REASON = new XContentBuilderString("reason"); + XContentBuilderString REQUEST = new XContentBuilderString("request"); + XContentBuilderString RESPONSE = new XContentBuilderString("response"); + XContentBuilderString MESSAGE = new XContentBuilderString("message"); + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccount.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccount.java new file mode 100644 index 00000000000..7260900cb5c --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccount.java @@ -0,0 +1,172 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Color; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Format; +import org.elasticsearch.watcher.support.http.*; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class UserAccount extends HipChatAccount { + + public static final String TYPE = "user"; + + final Defaults defaults; + + public UserAccount(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + super(name, Profile.USER, settings, defaultServer, httpClient, logger); + defaults = new Defaults(settings); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public void validateParsedTemplate(String watchId, String actionId, HipChatMessage.Template template) throws SettingsException { + if (template.from != null) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "]. [" + name + "] hipchat account doesn't support custom `from` fields"); + } + } + + @Override + public HipChatMessage render(String watchId, String actionId, TemplateEngine engine, HipChatMessage.Template template, Map model) { + String[] rooms = defaults.rooms; + if (template.rooms != null) { + rooms = new String[template.rooms.length]; + for (int i = 0; i < template.rooms.length; i++) { + rooms[i] = engine.render(template.rooms[i], model); + } + } + String[] users = defaults.users; + if (template.users != null) { + users = new String[template.users.length]; + for (int i = 0; i < template.users.length; i++) { + users[i] = engine.render(template.users[i], model); + } + } + String message = engine.render(template.body, model); + Color color = Color.resolve(engine.render(template.color, model), defaults.color); + Boolean notify = template.notify != null ? template.notify : defaults.notify; + Format messageFormat = template.format != null ? template.format : defaults.format; + return new HipChatMessage(message, rooms, users, null, messageFormat, color, notify); + } + + @Override + public SentMessages send(HipChatMessage message) { + List sentMessages = new ArrayList<>(); + if (message.rooms != null) { + for (String room : message.rooms) { + HttpRequest request = buildRoomRequest(room, message); + try { + HttpResponse response = httpClient.execute(request); + sentMessages.add(SentMessages.SentMessage.responded(room, SentMessages.SentMessage.TargetType.ROOM, message, request, response)); + } catch (IOException e) { + logger.error("failed to execute hipchat api http request", e); + sentMessages.add(SentMessages.SentMessage.error(room, SentMessages.SentMessage.TargetType.ROOM, message, ExceptionsHelper.detailedMessage(e))); + } + } + } + if (message.users != null) { + for (String user : message.users) { + HttpRequest request = buildUserRequest(user, message); + try { + HttpResponse response = httpClient.execute(request); + sentMessages.add(SentMessages.SentMessage.responded(user, SentMessages.SentMessage.TargetType.USER, message, request, response)); + } catch (IOException e) { + logger.error("failed to execute hipchat api http request", e); + sentMessages.add(SentMessages.SentMessage.error(user, SentMessages.SentMessage.TargetType.USER, message, ExceptionsHelper.detailedMessage(e))); + } + } + } + return new SentMessages(name, sentMessages); + } + + public HttpRequest buildRoomRequest(String room, final HipChatMessage message) { + return server.httpRequest() + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/room/" + room + "/notification") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer " + authToken) + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder xbuilder, Params params) throws IOException { + xbuilder.field("message", message.body); + if (message.format != null) { + xbuilder.field("message_format", message.format.value()); + } + if (message.notify != null) { + xbuilder.field("notify", message.notify); + } + if (message.color != null) { + xbuilder.field("color", String.valueOf(message.color.value())); + } + return xbuilder; + } + })) + .build(); + } + + public HttpRequest buildUserRequest(String user, final HipChatMessage message) { + return server.httpRequest() + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/user/" + user + "/message") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer " + authToken) + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder xbuilder, Params params) throws IOException { + xbuilder.field("message", message.body); + if (message.format != null) { + xbuilder.field("message_format", message.format.value()); + } + if (message.notify != null) { + xbuilder.field("notify", message.notify); + } + return xbuilder; + } + })) + .build(); + } + + static class Defaults { + + final @Nullable String[] rooms; + final @Nullable String[] users; + final @Nullable Format format; + final @Nullable Color color; + final @Nullable Boolean notify; + + public Defaults(Settings settings) { + this.rooms = settings.getAsArray(DEFAULT_ROOM_SETTING, null); + this.users = settings.getAsArray(DEFAULT_USER_SETTING, null); + this.format = Format.resolve(settings, DEFAULT_FORMAT_SETTING, null); + this.color = Color.resolve(settings, DEFAULT_COLOR_SETTING, null); + this.notify = settings.getAsBoolean(DEFAULT_NOTIFY_SETTING, null); + } + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/V1Account.java b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/V1Account.java new file mode 100644 index 00000000000..c4d4358e900 --- /dev/null +++ b/watcher/src/main/java/org/elasticsearch/watcher/actions/hipchat/service/V1Account.java @@ -0,0 +1,130 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Color; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage.Format; +import org.elasticsearch.watcher.support.http.*; +import org.elasticsearch.watcher.support.template.TemplateEngine; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class V1Account extends HipChatAccount { + + public static final String TYPE = "v1"; + + final Defaults defaults; + + public V1Account(String name, Settings settings, HipChatServer defaultServer, HttpClient httpClient, ESLogger logger) { + super(name, Profile.V1, settings, defaultServer, httpClient, logger); + defaults = new Defaults(settings); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public void validateParsedTemplate(String watchId, String actionId, HipChatMessage.Template template) throws ElasticsearchParseException { + if (template.users != null) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "]. [" + name + "] hipchat account doesn't support user private messaging"); + } + if ((template.rooms == null || template.rooms.length == 0) && (defaults.rooms == null || defaults.rooms.length == 0)) { + throw new ElasticsearchParseException("invalid [" + HipChatAction.TYPE + "] action for [" + watchId + "/" + actionId + "]. missing required [" + HipChatMessage.Field.ROOM + "] field for [" + name + "] hipchat account"); + } + } + + @Override + public HipChatMessage render(String watchId, String actionId, TemplateEngine engine, HipChatMessage.Template template, Map model) { + String message = engine.render(template.body, model); + String[] rooms = defaults.rooms; + if (template.rooms != null) { + rooms = new String[template.rooms.length]; + for (int i = 0; i < template.rooms.length; i++) { + rooms[i] = engine.render(template.rooms[i], model); + } + } + String from = template.from != null ? template.from : defaults.from != null ? defaults.from : watchId; + Color color = Color.resolve(engine.render(template.color, model), defaults.color); + Boolean notify = template.notify != null ? template.notify : defaults.notify; + Format messageFormat = template.format != null ? template.format : defaults.format; + return new HipChatMessage(message, rooms, null, from, messageFormat, color, notify); + } + + @Override + public SentMessages send(HipChatMessage message) { + List sentMessages = new ArrayList<>(); + if (message.rooms != null) { + for (String room : message.rooms) { + HttpRequest request = buildRoomRequest(room, message); + try { + HttpResponse response = httpClient.execute(request); + sentMessages.add(SentMessages.SentMessage.responded(room, SentMessages.SentMessage.TargetType.ROOM, message, request, response)); + } catch (IOException e) { + logger.error("failed to execute hipchat api http request", e); + sentMessages.add(SentMessages.SentMessage.error(room, SentMessages.SentMessage.TargetType.ROOM, message, ExceptionsHelper.detailedMessage(e))); + } + } + } + return new SentMessages(name, sentMessages); + } + + public HttpRequest buildRoomRequest(String room, HipChatMessage message) { + HttpRequest.Builder builder = server.httpRequest(); + builder.method(HttpMethod.POST); + builder.scheme(Scheme.HTTPS); + builder.path("/v1/rooms/message"); + builder.setHeader("Content-Type", "application/x-www-form-urlencoded"); + builder.setParam("format", "json"); + builder.setParam("auth_token", authToken); + + StringBuilder body = new StringBuilder(); + body.append("room_id=").append(room); + body.append("&from=").append(HttpRequest.encodeUrl(message.from)); + body.append("&message=").append(HttpRequest.encodeUrl(message.body)); + if (message.format != null) { + body.append("&message_format=").append(message.format.value()); + } + if (message.color != null) { + body.append("&color=").append(message.color.value()); + } + if (message.notify != null) { + body.append("¬ify=").append(message.notify ? "1" : "0"); + } + builder.body(body.toString()); + return builder.build(); + } + + static class Defaults { + + final @Nullable String[] rooms; + final @Nullable String from; + final @Nullable Format format; + final @Nullable Color color; + final @Nullable Boolean notify; + + public Defaults(Settings settings) { + this.rooms = settings.getAsArray(DEFAULT_ROOM_SETTING, null); + this.from = settings.get(DEFAULT_FROM_SETTING); + this.format = Format.resolve(settings, DEFAULT_FORMAT_SETTING, null); + this.color = Color.resolve(settings, DEFAULT_COLOR_SETTING, null); + this.notify = settings.getAsBoolean(DEFAULT_NOTIFY_SETTING, null); + } + } +} diff --git a/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpRequest.java b/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpRequest.java index 398c7bded70..6539c46f52f 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpRequest.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpRequest.java @@ -21,6 +21,9 @@ import org.elasticsearch.watcher.support.http.auth.HttpAuth; import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.Map; public class HttpRequest implements ToXContent { @@ -101,6 +104,22 @@ public class HttpRequest implements ToXContent { return readTimeout; } + public static String encodeUrl(String text) { + try { + return URLEncoder.encode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("failed to URL encode text [" + text + "]", e); + } + } + + public static String decodeUrl(String text) { + try { + return URLDecoder.decode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("failed to URL decode text [" + text + "]", e); + } + } + @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(); @@ -171,16 +190,28 @@ public class HttpRequest implements ToXContent { @Override public String toString() { - return "HttpRequest{" + - "auth=[" + (auth != null ? "******" : null) + - "], body=[" + body + '\'' + - "], path=[" + path + '\'' + - "], method=[" + method + - "], port=[" + port + - "], host=[" + host + '\'' + - "], connection_timeout=[" + connectionTimeout + '\'' + - "], read_timeout=[" + readTimeout + '\'' + - "]}"; + StringBuilder sb = new StringBuilder(); + sb.append("method=[").append(method).append("], "); + sb.append("scheme=[").append(scheme).append("], "); + sb.append("host=[").append(host).append("], "); + sb.append("port=[").append(port).append("], "); + sb.append("path=[").append(path).append("], "); + if (!headers.isEmpty()) { + sb.append(", headers=["); + boolean first = true; + for (Map.Entry header : headers.entrySet()) { + if (!first) { + sb.append(", "); + } + sb.append("[").append(header.getKey()).append(": ").append(header.getValue()).append("]"); + first = false; + } + sb.append("], "); + } + sb.append("connection_timeout=[").append(connectionTimeout).append("], "); + sb.append("read_timeout=[").append(readTimeout).append("], "); + sb.append("body=[").append(body).append("], "); + return sb.toString(); } public static Builder builder(String host, int port) { diff --git a/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java b/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java index 9beea639871..f32592615b3 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java @@ -20,6 +20,7 @@ import org.jboss.netty.handler.codec.http.HttpHeaders; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -111,6 +112,28 @@ public class HttpResponse implements ToXContent { return result; } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("status=[").append(status).append("]"); + if (!headers.isEmpty()) { + sb.append(", headers=["); + boolean first = true; + for (Map.Entry header : headers.entrySet()) { + if (!first) { + sb.append(", "); + } + sb.append("[").append(header.getKey()).append(": ").append(Arrays.toString(header.getValue())).append("]"); + first = false; + } + sb.append("]"); + } + if (hasContent()) { + sb.append(", body=[").append(body.toUtf8()).append("]"); + } + return sb.toString(); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder = builder.startObject().field(Field.STATUS.getPreferredName(), status); diff --git a/watcher/src/main/java/org/elasticsearch/watcher/support/xcontent/WatcherXContentUtils.java b/watcher/src/main/java/org/elasticsearch/watcher/support/xcontent/WatcherXContentUtils.java index 5899d39f5cc..e449af36278 100644 --- a/watcher/src/main/java/org/elasticsearch/watcher/support/xcontent/WatcherXContentUtils.java +++ b/watcher/src/main/java/org/elasticsearch/watcher/support/xcontent/WatcherXContentUtils.java @@ -8,11 +8,6 @@ package org.elasticsearch.watcher.support.xcontent; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.compress.CompressedStreamInput; -import org.elasticsearch.common.compress.Compressor; -import org.elasticsearch.common.compress.CompressorFactory; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -38,6 +33,29 @@ public class WatcherXContentUtils { } } + public static String[] readStringArray(XContentParser parser, boolean allowNull) throws IOException { + if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { + if (allowNull) { + return null; + } + throw new ElasticsearchParseException("could not parse [{}] field. expected a string array but found null value instead", parser.currentName()); + } + if (parser.currentToken() != XContentParser.Token.START_ARRAY) { + throw new ElasticsearchParseException("could not parse [{}] field. expected a string array but found [{}] value instead", parser.currentName(), parser.currentToken()); + } + + List list = new ArrayList<>(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token == XContentParser.Token.VALUE_STRING) { + list.add(parser.text()); + } else { + throw new ElasticsearchParseException("could not parse [{}] field. expected a string array but one of the value in the array is [{}]", parser.currentName(), token); + } + } + return list.toArray(new String[list.size()]); + } + // TODO open this up in core public static List readList(XContentParser parser, XContentParser.Token token) throws IOException { List list = new ArrayList<>(); diff --git a/watcher/src/main/resources/watch_history.json b/watcher/src/main/resources/watch_history.json index ed089698610..b3484f4164a 100644 --- a/watcher/src/main/resources/watch_history.json +++ b/watcher/src/main/resources/watch_history.json @@ -321,6 +321,69 @@ } } } + }, + "hipchat" : { + "type": "object", + "dynamic": true, + "properties": { + "account": { + "type": "string", + "index": "not_analyzed" + }, + "sent_messages": { + "type": "nested", + "include_in_parent": true, + "dynamic": true, + "properties": { + "status": { + "type": "string", + "index": "not_analyzed" + }, + "reason": { + "type": "string" + }, + "request" : { + "type" : "object", + "enabled" : false + }, + "response" : { + "type" : "object", + "enabled" : false + }, + "room" : { + "type": "string", + "index": "not_analyzed" + }, + "user" : { + "type": "string", + "index": "not_analyzed" + }, + "message" : { + "type" : "object", + "dynamic" : true, + "properties" : { + "message_format" : { + "type" : "string", + "index" : "not_analyzed" + }, + "color" : { + "type" : "string", + "index" : "not_analyzed" + }, + "notify" : { + "type" : "boolean" + }, + "message" : { + "type" : "string" + }, + "from" : { + "type" : "string" + } + } + } + } + } + } } } } diff --git a/watcher/src/test/java/org/elasticsearch/watcher/WatcherF.java b/watcher/src/test/java/org/elasticsearch/watcher/WatcherF.java index dd31badaaac..e95d230a894 100644 --- a/watcher/src/test/java/org/elasticsearch/watcher/WatcherF.java +++ b/watcher/src/test/java/org/elasticsearch/watcher/WatcherF.java @@ -24,6 +24,22 @@ public class WatcherF { System.setProperty("es.shield.enabled", "false"); System.setProperty("es.security.manager.enabled", "false"); System.setProperty("es.plugins.load_classpath_plugins", "false"); + + // this is for the `test-watcher-integration` group level integration in HipChat + System.setProperty("es.watcher.actions.hipchat.service.account.integration.profile", "integration"); + System.setProperty("es.watcher.actions.hipchat.service.account.integration.auth_token", "huuS9v7ccuOy3ZBWWWr1vt8Lqu3sQnLUE81nrLZU"); + System.setProperty("es.watcher.actions.hipchat.service.account.integration.room", "test-watcher"); + + // this is for the Watcher Test account in HipChat + System.setProperty("es.watcher.actions.hipchat.service.account.user.profile", "user"); + System.setProperty("es.watcher.actions.hipchat.service.account.user.auth_token", "FYVx16oDH78ZW9r13wtXbcszyoyA7oX5tiMWg9X0"); + + // this is for the `test-watcher-v1` notification token + System.setProperty("es.watcher.actions.hipchat.service.account.v1.profile", "v1"); + System.setProperty("es.watcher.actions.hipchat.service.account.v1.auth_token", "a734baf62df618b96dda55b323fc30"); + + + System.setProperty("es.plugin.types", WatcherPlugin.class.getName() + "," + LicensePlugin.class.getName()); System.setProperty("es.cluster.name", WatcherF.class.getSimpleName()); diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactoryTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactoryTests.java new file mode 100644 index 00000000000..788797d2561 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionFactoryTests.java @@ -0,0 +1,209 @@ +/* + * 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.watcher.actions.hipchat; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +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.test.ESTestCase; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatAccount; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatService; +import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.support.template.TemplateEngine; +import org.junit.Before; +import org.junit.Test; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.watcher.actions.ActionBuilders.hipchatAction; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.*; + +/** + * + */ +public class HipChatActionFactoryTests extends ESTestCase { + + private HipChatActionFactory factory; + private HipChatService hipchatService; + + @Before + public void init() throws Exception { + hipchatService = mock(HipChatService.class); + factory = new HipChatActionFactory(Settings.EMPTY, mock(TemplateEngine.class), hipchatService); + } + + @Test + public void testParseAction() throws Exception { + + HipChatAccount account = mock(HipChatAccount.class); + when(hipchatService.getAccount("_account1")).thenReturn(account); + + HipChatAction action = hipchatAction("_account1", "_body").build(); + XContentBuilder jsonBuilder = jsonBuilder().value(action); + XContentParser parser = JsonXContent.jsonXContent.createParser(jsonBuilder.bytes()); + parser.nextToken(); + + HipChatAction parsedAction = factory.parseAction("_w1", "_a1", parser); + assertThat(parsedAction, is(action)); + + verify(account, times(1)).validateParsedTemplate("_w1", "_a1", action.message); + } + + @Test(expected = ElasticsearchParseException.class) + public void testtestParseAction_UnknownAccount() throws Exception { + + when(hipchatService.getAccount("_unknown")).thenReturn(null); + + HipChatAction action = hipchatAction("_unknown", "_body").build(); + XContentBuilder jsonBuilder = jsonBuilder().value(action); + XContentParser parser = JsonXContent.jsonXContent.createParser(jsonBuilder.bytes()); + parser.nextToken(); + factory.parseAction("_w1", "_a1", parser); + } + + @Test + public void testParser() throws Exception { + + XContentBuilder builder = jsonBuilder().startObject(); + + String accountName = randomAsciiOfLength(10); + builder.field("account", accountName); + builder.startObject("message"); + + Template body = Template.inline("_body").build(); + builder.field("body", body); + + Template[] rooms = null; + if (randomBoolean()) { + Template r1 = Template.inline("_r1").build(); + Template r2 = Template.inline("_r2").build(); + rooms = new Template[] { r1, r2 }; + builder.array("room", r1, r2); + } + Template[] users = null; + if (randomBoolean()) { + Template u1 = Template.inline("_u1").build(); + Template u2 = Template.inline("_u2").build(); + users = new Template[] { u1, u2 }; + builder.array("user", u1, u2); + } + String from = null; + if (randomBoolean()) { + from = randomAsciiOfLength(10); + builder.field("from", from); + } + HipChatMessage.Format format = null; + if (randomBoolean()) { + format = randomFrom(HipChatMessage.Format.values()); + builder.field("format", format.value()); + } + Template color = null; + if (randomBoolean()) { + color = Template.inline(randomFrom(HipChatMessage.Color.values()).value()).build(); + builder.field("color", color); + } + Boolean notify = null; + if (randomBoolean()) { + notify = randomBoolean(); + builder.field("notify", notify); + } + + builder.endObject(); + builder.endObject(); + + BytesReference bytes = builder.bytes(); + logger.info("hipchat action json [{}]", bytes.toUtf8()); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatAction action = HipChatAction.parse("_watch", "_action", parser); + + assertThat(action, notNullValue()); + assertThat(action.account, is(accountName)); + assertThat(action.message, notNullValue()); + assertThat(action.message, is(new HipChatMessage.Template(body, rooms, users, from, format, color, notify))); + } + + + @Test + public void testParser_SelfGenerated() throws Exception { + + String accountName = randomAsciiOfLength(10); + Template body = Template.inline("_body").build(); + HipChatMessage.Template.Builder templateBuilder = new HipChatMessage.Template.Builder(body); + + XContentBuilder builder = jsonBuilder().startObject(); + builder.field("account", accountName); + builder.startObject("message"); + builder.field("body", body); + + if (randomBoolean()) { + Template r1 = Template.inline("_r1").build(); + Template r2 = Template.inline("_r2").build(); + templateBuilder.addRooms(r1, r2); + builder.array("room", r1, r2); + } + if (randomBoolean()) { + Template u1 = Template.inline("_u1").build(); + Template u2 = Template.inline("_u2").build(); + templateBuilder.addUsers(u1, u2); + builder.array("user", u1, u2); + } + if (randomBoolean()) { + String from = randomAsciiOfLength(10); + templateBuilder.setFrom(from); + builder.field("from", from); + } + if (randomBoolean()) { + HipChatMessage.Format format = randomFrom(HipChatMessage.Format.values()); + templateBuilder.setFormat(format); + builder.field("format", format.value()); + } + if (randomBoolean()) { + Template color = Template.inline(randomFrom(HipChatMessage.Color.values()).value()).build(); + templateBuilder.setColor(color); + builder.field("color", color); + } + if (randomBoolean()) { + boolean notify = randomBoolean(); + templateBuilder.setNotify(notify); + builder.field("notify", notify); + } + + builder.endObject(); + builder.endObject(); + + HipChatMessage.Template template = templateBuilder.build(); + + HipChatAction action = new HipChatAction(accountName, template); + + XContentBuilder jsonBuilder = jsonBuilder(); + action.toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + BytesReference bytes = builder.bytes(); + logger.info(bytes.toUtf8()); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatAction parsedAction = HipChatAction.parse("_watch", "_action", parser); + + assertThat(parsedAction, notNullValue()); + assertThat(parsedAction, is(action)); + } + + @Test(expected = ElasticsearchParseException.class) + public void testParser_Invalid() throws Exception { + XContentBuilder builder = jsonBuilder().startObject().field("unknown_field", "value"); + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + HipChatAction.parse("_watch", "_action", parser); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionTests.java new file mode 100644 index 00000000000..a28d1253023 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/HipChatActionTests.java @@ -0,0 +1,262 @@ +/* + * 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.watcher.actions.hipchat; + +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.MapBuilder; +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.test.ESTestCase; +import org.elasticsearch.watcher.actions.Action; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatAccount; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatMessage; +import org.elasticsearch.watcher.actions.hipchat.service.HipChatService; +import org.elasticsearch.watcher.actions.hipchat.service.SentMessages; +import org.elasticsearch.watcher.execution.WatchExecutionContext; +import org.elasticsearch.watcher.execution.Wid; +import org.elasticsearch.watcher.support.http.HttpRequest; +import org.elasticsearch.watcher.support.http.HttpResponse; +import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.support.template.TemplateEngine; +import org.elasticsearch.watcher.watch.Payload; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.watcher.test.WatcherTestUtils.mockExecutionContextBuilder; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + */ +public class HipChatActionTests extends ESTestCase { + + private HipChatService service; + + @Before + public void init() throws Exception { + service = mock(HipChatService.class); + } + + @Test + public void testExecute() throws Exception { + final String accountName = "account1"; + + TemplateEngine templateEngine = mock(TemplateEngine.class); + + Template body = Template.inline("_body").build(); + HipChatMessage.Template.Builder messageBuilder = new HipChatMessage.Template.Builder(body); + + HipChatMessage.Template messageTemplate = messageBuilder.build(); + + HipChatAction action = new HipChatAction(accountName, messageTemplate); + ExecutableHipChatAction executable = new ExecutableHipChatAction(action, logger, service, templateEngine); + + Map data = new HashMap<>(); + Payload payload = new Payload.Simple(data); + + Map metadata = MapBuilder.newMapBuilder().put("_key", "_val").map(); + + DateTime now = DateTime.now(DateTimeZone.UTC); + + Wid wid = new Wid(randomAsciiOfLength(5), randomLong(), now); + WatchExecutionContext ctx = mockExecutionContextBuilder(wid.watchId()) + .wid(wid) + .payload(payload) + .time(wid.watchId(), now) + .metadata(metadata) + .buildMock(); + + Map expectedModel = ImmutableMap.builder() + .put("ctx", ImmutableMap.builder() + .put("id", ctx.id().value()) + .put("watch_id", wid.watchId()) + .put("payload", data) + .put("metadata", metadata) + .put("execution_time", now) + .put("trigger", ImmutableMap.builder() + .put("triggered_time", now) + .put("scheduled_time", now) + .build()) + .put("vars", Collections.emptyMap()) + .build()) + .build(); + + if (body != null) { + when(templateEngine.render(body, expectedModel)).thenReturn(body.getTemplate()); + } + + String[] rooms = new String[] { "_r1" }; + HipChatMessage message = new HipChatMessage(body.getTemplate(), rooms, null, null, null, null, null); + HipChatAccount account = mock(HipChatAccount.class); + when(account.render(wid.watchId(), "_id", templateEngine, messageTemplate, expectedModel)).thenReturn(message); + HttpResponse response = mock(HttpResponse.class); + when(response.status()).thenReturn(200); + HttpRequest request = mock(HttpRequest.class); + SentMessages sentMessages = new SentMessages(accountName, Arrays.asList( + SentMessages.SentMessage.responded("_r1", SentMessages.SentMessage.TargetType.ROOM, message, request, response) + )); + when(account.send(message)).thenReturn(sentMessages); + when(service.getAccount(accountName)).thenReturn(account); + + Action.Result result = executable.execute("_id", ctx, payload); + + assertThat(result, notNullValue()); + assertThat(result, instanceOf(HipChatAction.Result.Executed.class)); + assertThat(result.status(), equalTo(Action.Result.Status.SUCCESS)); + assertThat(((HipChatAction.Result.Executed) result).sentMessages(), sameInstance(sentMessages)); + } + + @Test + public void testParser() throws Exception { + + XContentBuilder builder = jsonBuilder().startObject(); + + String accountName = randomAsciiOfLength(10); + builder.field("account", accountName); + builder.startObject("message"); + + Template body = Template.inline("_body").build(); + builder.field("body", body); + + Template[] rooms = null; + if (randomBoolean()) { + Template r1 = Template.inline("_r1").build(); + Template r2 = Template.inline("_r2").build(); + rooms = new Template[] { r1, r2 }; + builder.array("room", r1, r2); + } + Template[] users = null; + if (randomBoolean()) { + Template u1 = Template.inline("_u1").build(); + Template u2 = Template.inline("_u2").build(); + users = new Template[] { u1, u2 }; + builder.array("user", u1, u2); + } + String from = null; + if (randomBoolean()) { + from = randomAsciiOfLength(10); + builder.field("from", from); + } + HipChatMessage.Format format = null; + if (randomBoolean()) { + format = randomFrom(HipChatMessage.Format.values()); + builder.field("format", format.value()); + } + Template color = null; + if (randomBoolean()) { + color = Template.inline(randomFrom(HipChatMessage.Color.values()).value()).build(); + builder.field("color", color); + } + Boolean notify = null; + if (randomBoolean()) { + notify = randomBoolean(); + builder.field("notify", notify); + } + + builder.endObject(); + builder.endObject(); + + BytesReference bytes = builder.bytes(); + logger.info("hipchat action json [{}]", bytes.toUtf8()); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatAction action = HipChatAction.parse("_watch", "_action", parser); + + assertThat(action, notNullValue()); + assertThat(action.account, is(accountName)); + assertThat(action.message, notNullValue()); + assertThat(action.message, is(new HipChatMessage.Template(body, rooms, users, from, format, color, notify))); + } + + + @Test + public void testParser_SelfGenerated() throws Exception { + + String accountName = randomAsciiOfLength(10); + Template body = Template.inline("_body").build(); + HipChatMessage.Template.Builder templateBuilder = new HipChatMessage.Template.Builder(body); + + XContentBuilder builder = jsonBuilder().startObject(); + builder.field("account", accountName); + builder.startObject("message"); + builder.field("body", body); + + if (randomBoolean()) { + Template r1 = Template.inline("_r1").build(); + Template r2 = Template.inline("_r2").build(); + templateBuilder.addRooms(r1, r2); + builder.array("room", r1, r2); + } + if (randomBoolean()) { + Template u1 = Template.inline("_u1").build(); + Template u2 = Template.inline("_u2").build(); + templateBuilder.addUsers(u1, u2); + builder.array("user", u1, u2); + } + if (randomBoolean()) { + String from = randomAsciiOfLength(10); + templateBuilder.setFrom(from); + builder.field("from", from); + } + if (randomBoolean()) { + HipChatMessage.Format format = randomFrom(HipChatMessage.Format.values()); + templateBuilder.setFormat(format); + builder.field("format", format.value()); + } + if (randomBoolean()) { + Template color = Template.inline(randomFrom(HipChatMessage.Color.values()).value()).build(); + templateBuilder.setColor(color); + builder.field("color", color); + } + if (randomBoolean()) { + boolean notify = randomBoolean(); + templateBuilder.setNotify(notify); + builder.field("notify", notify); + } + + builder.endObject(); + builder.endObject(); + + HipChatMessage.Template template = templateBuilder.build(); + + HipChatAction action = new HipChatAction(accountName, template); + + XContentBuilder jsonBuilder = jsonBuilder(); + action.toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + BytesReference bytes = builder.bytes(); + logger.info(bytes.toUtf8()); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatAction parsedAction = HipChatAction.parse("_watch", "_action", parser); + + assertThat(parsedAction, notNullValue()); + assertThat(parsedAction, is(action)); + } + + @Test(expected = ElasticsearchParseException.class) + public void testParser_Invalid() throws Exception { + XContentBuilder builder = jsonBuilder().startObject().field("unknown_field", "value"); + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + HipChatAction.parse("_watch", "_action", parser); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccountsTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccountsTests.java new file mode 100644 index 00000000000..9534e8be11a --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatAccountsTests.java @@ -0,0 +1,129 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.support.http.HttpClient; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; + +/** + * + */ +public class HipChatAccountsTests extends ESTestCase { + + private HttpClient httpClient; + + @Before + public void init() throws Exception { + httpClient = mock(HttpClient.class); + } + + @Test + public void testSingleAccount() throws Exception { + Settings.Builder builder = Settings.builder() + .put("default_account", "account1"); + addAccountSettings("account1", builder); + + HipChatAccounts accounts = new HipChatAccounts(builder.build(), httpClient, logger); + HipChatAccount account = accounts.account("account1"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + account = accounts.account(null); // falling back on the default + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + } + + @Test + public void testSingleAccount_NoExplicitDefault() throws Exception { + Settings.Builder builder = Settings.builder(); + addAccountSettings("account1", builder); + + HipChatAccounts accounts = new HipChatAccounts(builder.build(), httpClient, logger); + HipChatAccount account = accounts.account("account1"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + account = accounts.account(null); // falling back on the default + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + } + + @Test + public void testMultipleAccounts() throws Exception { + Settings.Builder builder = Settings.builder() + .put("default_account", "account1"); + addAccountSettings("account1", builder); + addAccountSettings("account2", builder); + + HipChatAccounts accounts = new HipChatAccounts(builder.build(), httpClient, logger); + HipChatAccount account = accounts.account("account1"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + account = accounts.account("account2"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account2")); + account = accounts.account(null); // falling back on the default + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + } + + @Test + public void testMultipleAccounts_NoExplicitDefault() throws Exception { + Settings.Builder builder = Settings.builder() + .put("default_account", "account1"); + addAccountSettings("account1", builder); + addAccountSettings("account2", builder); + + HipChatAccounts accounts = new HipChatAccounts(builder.build(), httpClient, logger); + HipChatAccount account = accounts.account("account1"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account1")); + account = accounts.account("account2"); + assertThat(account, notNullValue()); + assertThat(account.name, equalTo("account2")); + account = accounts.account(null); + assertThat(account, notNullValue()); + assertThat(account.name, isOneOf("account1", "account2")); + } + + @Test(expected = SettingsException.class) + public void testMultipleAccounts_UnknownDefault() throws Exception { + Settings.Builder builder = Settings.builder() + .put("default_account", "unknown"); + addAccountSettings("account1", builder); + addAccountSettings("account2", builder); + new HipChatAccounts(builder.build(), httpClient, logger); + } + + @Test(expected = IllegalStateException.class) + public void testNoAccount() throws Exception { + Settings.Builder builder = Settings.builder(); + HipChatAccounts accounts = new HipChatAccounts(builder.build(), httpClient, logger); + accounts.account(null); + fail("no accounts are configured so trying to get the default account should throw an IllegalStateException"); + } + + @Test(expected = SettingsException.class) + public void testNoAccount_WithDefaultAccount() throws Exception { + Settings.Builder builder = Settings.builder() + .put("default_account", "unknown"); + new HipChatAccounts(builder.build(), httpClient, logger); + } + + private void addAccountSettings(String name, Settings.Builder builder) { + HipChatAccount.Profile profile = randomFrom(HipChatAccount.Profile.values()); + builder.put("account." + name + ".profile", profile.value()); + builder.put("account." + name + ".auth_token", randomAsciiOfLength(50)); + if (profile == HipChatAccount.Profile.INTEGRATION) { + builder.put("account." + name + ".room", randomAsciiOfLength(10)); + } + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessageTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessageTests.java new file mode 100644 index 00000000000..95af423c7c3 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatMessageTests.java @@ -0,0 +1,291 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.bytes.BytesReference; +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.test.ESTestCase; +import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.support.xcontent.WatcherXContentUtils; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.*; + +/** + * + */ +public class HipChatMessageTests extends ESTestCase { + + @Test + public void testToXContent() throws Exception { + String message = randomAsciiOfLength(10); + String[] rooms = generateRandomStringArray(3, 10, true); + String[] users = generateRandomStringArray(3, 10, true); + String from = randomBoolean() ? null : randomAsciiOfLength(10); + HipChatMessage.Format format = rarely() ? null : randomFrom(HipChatMessage.Format.values()); + HipChatMessage.Color color = rarely() ? null : randomFrom(HipChatMessage.Color.values()); + Boolean notify = rarely() ? null : randomBoolean(); + HipChatMessage msg = new HipChatMessage(message, rooms, users, from, format, color, notify); + + XContentBuilder builder = jsonBuilder(); + boolean includeTarget = randomBoolean(); + if (includeTarget && randomBoolean()) { + msg.toXContent(builder, ToXContent.EMPTY_PARAMS); + } else { + msg.toXContent(builder, ToXContent.EMPTY_PARAMS, includeTarget); + } + BytesReference bytes = builder.bytes(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + assertThat(parser.currentToken(), is(XContentParser.Token.START_OBJECT)); + + message = null; + rooms = null; + users = null; + from = null; + format = null; + color = null; + notify = null; + XContentParser.Token token = null; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if ("body".equals(currentFieldName)) { + message = parser.text(); + } else if ("room".equals(currentFieldName)) { + rooms = WatcherXContentUtils.readStringArray(parser, false); + } else if ("user".equals(currentFieldName)) { + users = WatcherXContentUtils.readStringArray(parser, false); + } else if ("from".equals(currentFieldName)) { + from = parser.text(); + } else if ("format".equals(currentFieldName)) { + format = HipChatMessage.Format.parse(parser); + } else if ("color".equals(currentFieldName)) { + color = HipChatMessage.Color.parse(parser); + } else if ("notify".equals(currentFieldName)) { + notify = parser.booleanValue(); + } else { + fail("unexpected xconent field [" + currentFieldName + "] in hipchat message"); + } + } + + assertThat(message, notNullValue()); + assertThat(message, is(msg.body)); + if (includeTarget) { + if (msg.rooms == null || msg.rooms.length == 0) { + assertThat(rooms, nullValue()); + } else { + assertThat(rooms, arrayContaining(msg.rooms)); + } + if (msg.users == null || msg.users.length == 0) { + assertThat(users, nullValue()); + } else { + assertThat(users, arrayContaining(msg.users)); + } + } + assertThat(from, is(msg.from)); + assertThat(format, is(msg.format)); + assertThat(color, is(msg.color)); + assertThat(notify, is(msg.notify)); + } + + @Test + public void testEquals() throws Exception { + String message = randomAsciiOfLength(10); + String[] rooms = generateRandomStringArray(3, 10, true); + String[] users = generateRandomStringArray(3, 10, true); + String from = randomBoolean() ? null : randomAsciiOfLength(10); + HipChatMessage.Format format = rarely() ? null : randomFrom(HipChatMessage.Format.values()); + HipChatMessage.Color color = rarely() ? null : randomFrom(HipChatMessage.Color.values()); + Boolean notify = rarely() ? null : randomBoolean(); + HipChatMessage msg1 = new HipChatMessage(message, rooms, users, from, format, color, notify); + + boolean equals = randomBoolean(); + if (!equals) { + equals = true; + if (rarely()) { + equals = false; + message = "another message"; + } + if (rarely()) { + equals = false; + rooms = rooms == null ? new String[] { "roomX" } : randomBoolean() ? null : new String[] { "roomX" , "roomY"}; + } + if (rarely()) { + equals = false; + users = users == null ? new String[] { "userX" } : randomBoolean() ? null : new String[] { "userX", "userY" }; + } + if (rarely()) { + equals = false; + from = from == null ? "fromX" : randomBoolean() ? null : "fromY"; + } + if (rarely()) { + equals = false; + format = format == null ? + randomFrom(HipChatMessage.Format.values()) : + randomBoolean() ? + null : + randomFrom(HipChatMessage.Format.values(), format); + } + if (rarely()) { + equals = false; + color = color == null ? + randomFrom(HipChatMessage.Color.values()) : + randomBoolean() ? + null : + randomFrom(HipChatMessage.Color.values(), color); + } + if (rarely()) { + equals = false; + notify = notify == null ? (Boolean) randomBoolean() : randomBoolean() ? null : (Boolean) randomBoolean(); + } + } + + HipChatMessage msg2 = new HipChatMessage(message, rooms, users, from, format, color, notify); + assertThat(msg1.equals(msg2), is(equals)); + } + + @Test + public void testTemplate_Parse() throws Exception { + XContentBuilder jsonBuilder = jsonBuilder(); + jsonBuilder.startObject(); + + Template body = Template.inline(randomAsciiOfLength(200)).build(); + jsonBuilder.field("body", body, ToXContent.EMPTY_PARAMS); + Template[] rooms = null; + if (randomBoolean()) { + jsonBuilder.startArray("room"); + rooms = new Template[randomIntBetween(1, 3)]; + for (int i = 0; i < rooms.length; i++) { + rooms[i] = Template.inline(randomAsciiOfLength(10)).build(); + rooms[i].toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + } + jsonBuilder.endArray(); + } + Template[] users = null; + if (randomBoolean()) { + jsonBuilder.startArray("user"); + users = new Template[randomIntBetween(1, 3)]; + for (int i = 0; i < users.length; i++) { + users[i] = Template.inline(randomAsciiOfLength(10)).build(); + users[i].toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + } + jsonBuilder.endArray(); + } + String from = null; + if (randomBoolean()) { + from = randomAsciiOfLength(10); + jsonBuilder.field("from", from); + } + Template color = null; + if (randomBoolean()) { + color = Template.inline(randomAsciiOfLength(10)).build(); + jsonBuilder.field("color", color, ToXContent.EMPTY_PARAMS); + } + HipChatMessage.Format format = null; + if (randomBoolean()) { + format = randomFrom(HipChatMessage.Format.values()); + jsonBuilder.field("format", format, ToXContent.EMPTY_PARAMS); + } + Boolean notify = null; + if (randomBoolean()) { + notify = randomBoolean(); + jsonBuilder.field("notify", notify); + } + + BytesReference bytes = jsonBuilder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatMessage.Template template = HipChatMessage.Template.parse(parser); + + assertThat(template, notNullValue()); + assertThat(template.body, is(body)); + if (rooms == null) { + assertThat(template.rooms, nullValue()); + } else { + assertThat(template.rooms, arrayContaining(rooms)); + } + if (users == null) { + assertThat(template.users, nullValue()); + } else { + assertThat(template.users, arrayContaining(users)); + } + assertThat(template.from, is(from)); + assertThat(template.color, is(color)); + assertThat(template.format, is(format)); + assertThat(template.notify, is(notify)); + } + + @Test + public void testTemplate_ParseSelfGenerated() throws Exception { + Template body = Template.inline(randomAsciiOfLength(10)).build(); + HipChatMessage.Template.Builder templateBuilder = new HipChatMessage.Template.Builder(body); + + if (randomBoolean()) { + int count = randomIntBetween(1, 3); + for (int i = 0; i < count; i++) { + templateBuilder.addRooms(Template.inline(randomAsciiOfLength(10)).build()); + } + } + if (randomBoolean()) { + int count = randomIntBetween(1, 3); + for (int i = 0; i < count; i++) { + templateBuilder.addUsers(Template.inline(randomAsciiOfLength(10)).build()); + } + } + if (randomBoolean()) { + templateBuilder.setFrom(randomAsciiOfLength(10)); + } + if (randomBoolean()) { + templateBuilder.setColor(Template.inline(randomAsciiOfLength(5)).build()); + } + if (randomBoolean()) { + templateBuilder.setFormat(randomFrom(HipChatMessage.Format.values())); + } + if (randomBoolean()) { + templateBuilder.setNotify(randomBoolean()); + } + HipChatMessage.Template template = templateBuilder.build(); + + XContentBuilder jsonBuilder = jsonBuilder(); + template.toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + BytesReference bytes = jsonBuilder.bytes(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + + HipChatMessage.Template parsed = HipChatMessage.Template.parse(parser); + + assertThat(parsed, equalTo(template)); + + } + + static E randomFrom(E[] values, E... exclude) { + List excludes = Arrays.asList(exclude); + List includes = new ArrayList<>(); + for (E value : values) { + if (!excludes.contains(value)) { + includes.add(value); + } + } + return randomFrom(includes); + } + +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServiceIT.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServiceIT.java new file mode 100644 index 00000000000..37b02ae8079 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/HipChatServiceIT.java @@ -0,0 +1,202 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.junit.annotations.Network; +import org.elasticsearch.watcher.actions.hipchat.HipChatAction; +import org.elasticsearch.watcher.test.AbstractWatcherIntegrationTests; +import org.elasticsearch.watcher.transport.actions.put.PutWatchResponse; +import org.junit.Before; +import org.junit.Test; + +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; +import static org.elasticsearch.watcher.actions.ActionBuilders.hipchatAction; +import static org.elasticsearch.watcher.client.WatchSourceBuilders.watchBuilder; +import static org.elasticsearch.watcher.condition.ConditionBuilders.alwaysCondition; +import static org.elasticsearch.watcher.input.InputBuilders.simpleInput; +import static org.elasticsearch.watcher.trigger.TriggerBuilders.schedule; +import static org.elasticsearch.watcher.trigger.schedule.Schedules.interval; +import static org.hamcrest.Matchers.*; + +/** + * + */ +@Network +public class HipChatServiceIT extends AbstractWatcherIntegrationTests { + + private HipChatService service; + + @Override + protected boolean timeWarped() { + return true; + } + + @Override + protected boolean enableShield() { + return false; + } + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + + // this is for the `test-watcher-integration` group level integration in HipChat + .put("watcher.actions.hipchat.service.account.integration_account.profile", "integration") + .put("watcher.actions.hipchat.service.account.integration_account.auth_token", "huuS9v7ccuOy3ZBWWWr1vt8Lqu3sQnLUE81nrLZU") + .put("watcher.actions.hipchat.service.account.integration_account.room", "test-watcher") + + // this is for the Watcher Test account in HipChat + .put("watcher.actions.hipchat.service.account.user_account.profile", "user") + .put("watcher.actions.hipchat.service.account.user_account.auth_token", "FYVx16oDH78ZW9r13wtXbcszyoyA7oX5tiMWg9X0") + + // this is for the `test-watcher-v1` notification token + .put("watcher.actions.hipchat.service.account.v1_account.profile", "v1") + .put("watcher.actions.hipchat.service.account.v1_account.auth_token", "a734baf62df618b96dda55b323fc30") + .build(); + } + + @Before + public void init() throws Exception { + service = getInstanceFromMaster(HipChatService.class); + } + + @Test + public void testSendMessage_V1Account() throws Exception { + HipChatMessage hipChatMessage = new HipChatMessage( + "/code HipChatServiceIT#testSendMessage_V1Account", + new String[] { "test-watcher", "test-watcher-2" }, + null, // users are unsupported in v1 + "watcher-tests", + HipChatMessage.Format.TEXT, + randomFrom(HipChatMessage.Color.values()), + true); + + HipChatAccount account = service.getAccount("v1_account"); + assertThat(account, notNullValue()); + SentMessages messages = account.send(hipChatMessage); + assertThat(messages.count(), is(2)); + for (SentMessages.SentMessage message : messages) { + assertThat(message.successful(), is(true)); + assertThat(message.request, notNullValue()); + assertThat(message.response, notNullValue()); + assertThat(message.response.status(), lessThan(300)); + } + } + + @Test + public void testSendMessage_IntegrationAccount() throws Exception { + HipChatMessage hipChatMessage = new HipChatMessage( + "/code HipChatServiceIT#testSendMessage_IntegrationAccount", + null, // custom rooms are unsupported by integration profiles + null, // users are unsupported by integration profiles + null, // custom "from" is not supported by integration profiles + HipChatMessage.Format.TEXT, + randomFrom(HipChatMessage.Color.values()), + true); + + HipChatAccount account = service.getAccount("integration_account"); + assertThat(account, notNullValue()); + SentMessages messages = account.send(hipChatMessage); + assertThat(messages.count(), is(1)); + for (SentMessages.SentMessage message : messages) { + assertThat(message.successful(), is(true)); + assertThat(message.request, notNullValue()); + assertThat(message.response, notNullValue()); + assertThat(message.response.status(), lessThan(300)); + } + } + + @Test + public void testSendMessage_UserAccount() throws Exception { + HipChatMessage hipChatMessage = new HipChatMessage( + "/code HipChatServiceIT#testSendMessage_UserAccount", + new String[] { "test-watcher", "test-watcher-2" }, + new String[] { "watcher@elastic.co" }, + null, // custom "from" is not supported by integration profiles + HipChatMessage.Format.TEXT, + randomFrom(HipChatMessage.Color.values()), + false); + + HipChatAccount account = service.getAccount("user_account"); + assertThat(account, notNullValue()); + SentMessages messages = account.send(hipChatMessage); + assertThat(messages.count(), is(3)); + for (SentMessages.SentMessage message : messages) { + assertThat(message.successful(), is(true)); + assertThat(message.request, notNullValue()); + assertThat(message.response, notNullValue()); + assertThat(message.response.status(), lessThan(300)); + } + } + + @Test + public void testWatchWithHipChatAction() throws Exception { + + + HipChatAccount.Profile profile = randomFrom(HipChatAccount.Profile.values()); + String account; + HipChatAction.Builder actionBuilder; + switch (profile) { + case USER: + account = "user_account"; + actionBuilder = hipchatAction(account, "/code {{ctx.payload.ref}}") + .addRooms("test-watcher", "test-watcher-2") + .addUsers("watcher@elastic.co") + .setFormat(HipChatMessage.Format.TEXT) + .setColor(randomFrom(HipChatMessage.Color.values())) + .setNotify(false); + break; + + case INTEGRATION: + account = "integration_account"; + actionBuilder = hipchatAction(account, "/code {{ctx.payload.ref}}") + .setFormat(HipChatMessage.Format.TEXT) + .setColor(randomFrom(HipChatMessage.Color.values())) + .setNotify(false); + break; + + default: + assertThat(profile, is(HipChatAccount.Profile.V1)); + account = "v1_account"; + actionBuilder = hipchatAction(account, "/code {{ctx.payload.ref}}") + .addRooms("test-watcher", "test-watcher-2") + .setFrom("watcher-test") + .setFormat(HipChatMessage.Format.TEXT) + .setColor(randomFrom(HipChatMessage.Color.values())) + .setNotify(false); + } + + PutWatchResponse putWatchResponse = watcherClient().preparePutWatch("1").setSource(watchBuilder() + .trigger(schedule(interval("10m"))) + .input(simpleInput("ref", "HipChatServiceIT#testWatchWithHipChatAction")) + .condition(alwaysCondition()) + .addAction("hipchat", actionBuilder)) + .execute().get(); + + assertThat(putWatchResponse.isCreated(), is(true)); + + timeWarp().scheduler().trigger("1"); + flush(); + refresh(); + + assertWatchWithMinimumPerformedActionsCount("1", 1L, false); + + SearchResponse response = searchHistory(searchSource().query(boolQuery() + .must(termQuery("result.actions.id", "hipchat")) + .must(termQuery("result.actions.type", "hipchat")) + .must(termQuery("result.actions.status", "success")) + .must(termQuery("result.actions.hipchat.account", account)) + .must(termQuery("result.actions.hipchat.sent_messages.status", "success")))); + + assertThat(response, notNullValue()); + assertThat(response.getHits().getTotalHits(), is(1L)); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccountTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccountTests.java new file mode 100644 index 00000000000..f1f633dbd81 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/IntegrationAccountTests.java @@ -0,0 +1,151 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.support.http.*; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; + +/** + * + */ +public class IntegrationAccountTests extends ESTestCase { + + @Test + public void testSettings() throws Exception { + String accountName = "_name"; + + Settings.Builder sb = Settings.builder(); + + String authToken = randomAsciiOfLength(50); + sb.put(IntegrationAccount.AUTH_TOKEN_SETTING, authToken); + + String host = HipChatServer.DEFAULT.host(); + if (randomBoolean()) { + host = randomAsciiOfLength(10); + sb.put(HipChatServer.HOST_SETTING, host); + } + int port = HipChatServer.DEFAULT.port(); + if (randomBoolean()) { + port = randomIntBetween(300, 400); + sb.put(HipChatServer.PORT_SETTING, port); + } + + String room = randomAsciiOfLength(10); + sb.put(IntegrationAccount.ROOM_SETTING, room); + + HipChatMessage.Format defaultFormat = null; + if (randomBoolean()) { + defaultFormat = randomFrom(HipChatMessage.Format.values()); + sb.put(HipChatAccount.DEFAULT_FORMAT_SETTING, defaultFormat); + } + HipChatMessage.Color defaultColor = null; + if (randomBoolean()) { + defaultColor = randomFrom(HipChatMessage.Color.values()); + sb.put(HipChatAccount.DEFAULT_COLOR_SETTING, defaultColor); + } + Boolean defaultNotify = null; + if (randomBoolean()) { + defaultNotify = randomBoolean(); + sb.put(HipChatAccount.DEFAULT_NOTIFY_SETTING, defaultNotify); + } + Settings settings = sb.build(); + + IntegrationAccount account = new IntegrationAccount(accountName, settings, HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + + assertThat(account.profile, is(HipChatAccount.Profile.INTEGRATION)); + assertThat(account.name, equalTo(accountName)); + assertThat(account.server.host(), is(host)); + assertThat(account.server.port(), is(port)); + assertThat(account.authToken, is(authToken)); + assertThat(account.room, is(room)); + assertThat(account.defaults.format, is(defaultFormat)); + assertThat(account.defaults.color, is(defaultColor)); + assertThat(account.defaults.notify, is(defaultNotify)); + } + + @Test(expected = SettingsException.class) + public void testSettings_NoAuthToken() throws Exception { + Settings.Builder sb = Settings.builder(); + sb.put(IntegrationAccount.ROOM_SETTING, randomAsciiOfLength(10)); + new IntegrationAccount("_name", sb.build(), HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + } + + @Test(expected = SettingsException.class) + public void testSettings_WithoutRoom() throws Exception { + Settings.Builder sb = Settings.builder(); + sb.put(IntegrationAccount.AUTH_TOKEN_SETTING, randomAsciiOfLength(50)); + new IntegrationAccount("_name", sb.build(), HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + } + + @Test(expected = SettingsException.class) + public void testSettings_WithoutMultipleRooms() throws Exception { + Settings.Builder sb = Settings.builder(); + sb.put(IntegrationAccount.AUTH_TOKEN_SETTING, randomAsciiOfLength(50)); + sb.put(IntegrationAccount.ROOM_SETTING, "_r1,_r2"); + new IntegrationAccount("_name", sb.build(), HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + } + + @Test + public void testSend() throws Exception { + HttpClient httpClient = mock(HttpClient.class); + IntegrationAccount account = new IntegrationAccount("_name", Settings.builder() + .put("host", "_host") + .put("port", "443") + .put("auth_token", "_token") + .put("room", "_room") + .build(), HipChatServer.DEFAULT, httpClient, mock(ESLogger.class)); + + HipChatMessage.Format format = randomFrom(HipChatMessage.Format.values()); + HipChatMessage.Color color = randomFrom(HipChatMessage.Color.values()); + Boolean notify = randomBoolean(); + final HipChatMessage message = new HipChatMessage("_body", null, null, null, format, color, notify); + + HttpRequest req = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/room/_room/notification") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer _token") + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("message", message.body); + if (message.format != null) { + builder.field("message_format", message.format.value()); + } + if (message.notify != null) { + builder.field("notify", message.notify); + } + if (message.color != null) { + builder.field("color", String.valueOf(message.color.value())); + } + return builder; + } + })) + .build(); + + HttpResponse res = mock(HttpResponse.class); + when(res.status()).thenReturn(200); + when(httpClient.execute(req)).thenReturn(res); + + account.send(message); + + verify(httpClient).execute(req); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatServiceTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatServiceTests.java new file mode 100644 index 00000000000..3647112e5a0 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/InternalHipChatServiceTests.java @@ -0,0 +1,281 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.node.settings.NodeSettingsService; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.shield.WatcherSettingsFilter; +import org.elasticsearch.watcher.support.http.HttpClient; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + +/** + * + */ +public class InternalHipChatServiceTests extends ESTestCase { + + private HttpClient httpClient; + private NodeSettingsService nodeSettingsService; + private WatcherSettingsFilter settingsFilter; + + @Before + public void init() throws Exception { + httpClient = mock(HttpClient.class); + nodeSettingsService = mock(NodeSettingsService.class); + settingsFilter = mock(WatcherSettingsFilter.class); + } + + @Test + public void testSingleAccount_V1() throws Exception { + String accountName = randomAsciiOfLength(10); + String host = randomBoolean() ? null : "_host"; + int port = randomBoolean() ? -1 : randomIntBetween(300, 400); + String defaultRoom = randomBoolean() ? null : "_r1, _r2"; + String defaultFrom = randomBoolean() ? null : "_from"; + HipChatMessage.Color defaultColor = randomBoolean() ? null : randomFrom(HipChatMessage.Color.values()); + HipChatMessage.Format defaultFormat = randomBoolean() ? null : randomFrom(HipChatMessage.Format.values()); + Boolean defaultNotify = randomBoolean() ? null : (Boolean) randomBoolean(); + Settings.Builder settingsBuilder = Settings.builder() + .put("watcher.actions.hipchat.service.account." + accountName + ".profile", HipChatAccount.Profile.V1.value()) + .put("watcher.actions.hipchat.service.account." + accountName + ".auth_token", "_token"); + if (host != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".host", host); + } + if (port > 0) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".port", port); + } + buildMessageDefaults(accountName, settingsBuilder, defaultRoom, null, defaultFrom, defaultColor, defaultFormat, defaultNotify); + InternalHipChatService service = new InternalHipChatService(settingsBuilder.build(), httpClient, nodeSettingsService, settingsFilter); + service.start(); + + HipChatAccount account = service.getAccount(accountName); + assertThat(account, notNullValue()); + assertThat(account.name, is(accountName)); + assertThat(account.authToken, is("_token")); + assertThat(account.profile, is(HipChatAccount.Profile.V1)); + assertThat(account.httpClient, is(httpClient)); + assertThat(account.server, notNullValue()); + assertThat(account.server.host(), is(host != null ? host : HipChatServer.DEFAULT.host())); + assertThat(account.server.port(), is(port > 0 ? port : HipChatServer.DEFAULT.port())); + assertThat(account, instanceOf(V1Account.class)); + if (defaultRoom == null) { + assertThat(((V1Account) account).defaults.rooms, nullValue()); + } else { + assertThat(((V1Account) account).defaults.rooms, arrayContaining("_r1", "_r2")); + } + assertThat(((V1Account) account).defaults.from, is(defaultFrom)); + assertThat(((V1Account) account).defaults.color, is(defaultColor)); + assertThat(((V1Account) account).defaults.format, is(defaultFormat)); + assertThat(((V1Account) account).defaults.notify, is(defaultNotify)); + + // with a single account defined, making sure that that account is set to the default one. + assertThat(service.getDefaultAccount(), sameInstance(account)); + + assertThatSettingsFilterWasAdded(); + } + + @Test + public void testSingleAccount_Integration() throws Exception { + String accountName = randomAsciiOfLength(10); + String host = randomBoolean() ? null : "_host"; + int port = randomBoolean() ? -1 : randomIntBetween(300, 400); + String room = randomAsciiOfLength(10); + String defaultFrom = randomBoolean() ? null : "_from"; + HipChatMessage.Color defaultColor = randomBoolean() ? null : randomFrom(HipChatMessage.Color.values()); + HipChatMessage.Format defaultFormat = randomBoolean() ? null : randomFrom(HipChatMessage.Format.values()); + Boolean defaultNotify = randomBoolean() ? null : (Boolean) randomBoolean(); + Settings.Builder settingsBuilder = Settings.builder() + .put("watcher.actions.hipchat.service.account." + accountName + ".profile", HipChatAccount.Profile.INTEGRATION.value()) + .put("watcher.actions.hipchat.service.account." + accountName + ".auth_token", "_token") + .put("watcher.actions.hipchat.service.account." + accountName + ".room", room); + if (host != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".host", host); + } + if (port > 0) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".port", port); + } + buildMessageDefaults(accountName, settingsBuilder, null, null, defaultFrom, defaultColor, defaultFormat, defaultNotify); + InternalHipChatService service = new InternalHipChatService(settingsBuilder.build(), httpClient, nodeSettingsService, settingsFilter); + service.start(); + + HipChatAccount account = service.getAccount(accountName); + assertThat(account, notNullValue()); + assertThat(account.name, is(accountName)); + assertThat(account.authToken, is("_token")); + assertThat(account.profile, is(HipChatAccount.Profile.INTEGRATION)); + assertThat(account.httpClient, is(httpClient)); + assertThat(account.server, notNullValue()); + assertThat(account.server.host(), is(host != null ? host : HipChatServer.DEFAULT.host())); + assertThat(account.server.port(), is(port > 0 ? port : HipChatServer.DEFAULT.port())); + assertThat(account, instanceOf(IntegrationAccount.class)); + assertThat(((IntegrationAccount) account).room, is(room)); + assertThat(((IntegrationAccount) account).defaults.color, is(defaultColor)); + assertThat(((IntegrationAccount) account).defaults.format, is(defaultFormat)); + assertThat(((IntegrationAccount) account).defaults.notify, is(defaultNotify)); + + // with a single account defined, making sure that that account is set to the default one. + assertThat(service.getDefaultAccount(), sameInstance(account)); + + assertThatSettingsFilterWasAdded(); + } + + @Test(expected = SettingsException.class) + public void testSingleAccount_Integration_NoRoomSetting() throws Exception { + String accountName = randomAsciiOfLength(10); + Settings.Builder settingsBuilder = Settings.builder() + .put("watcher.actions.hipchat.service.account." + accountName + ".profile", HipChatAccount.Profile.INTEGRATION.value()) + .put("watcher.actions.hipchat.service.account." + accountName + ".auth_token", "_token"); + InternalHipChatService service = new InternalHipChatService(settingsBuilder.build(), httpClient, nodeSettingsService, settingsFilter); + service.start(); + } + + @Test + public void testSingleAccount_User() throws Exception { + String accountName = randomAsciiOfLength(10); + String host = randomBoolean() ? null : "_host"; + int port = randomBoolean() ? -1 : randomIntBetween(300, 400); + String defaultRoom = randomBoolean() ? null : "_r1, _r2"; + String defaultUser = randomBoolean() ? null : "_u1, _u2"; + HipChatMessage.Color defaultColor = randomBoolean() ? null : randomFrom(HipChatMessage.Color.values()); + HipChatMessage.Format defaultFormat = randomBoolean() ? null : randomFrom(HipChatMessage.Format.values()); + Boolean defaultNotify = randomBoolean() ? null : (Boolean) randomBoolean(); + Settings.Builder settingsBuilder = Settings.builder() + .put("watcher.actions.hipchat.service.account." + accountName + ".profile", HipChatAccount.Profile.USER.value()) + .put("watcher.actions.hipchat.service.account." + accountName + ".auth_token", "_token"); + if (host != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".host", host); + } + if (port > 0) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + accountName + ".port", port); + } + buildMessageDefaults(accountName, settingsBuilder, defaultRoom, defaultUser, null, defaultColor, defaultFormat, defaultNotify); + InternalHipChatService service = new InternalHipChatService(settingsBuilder.build(), httpClient, nodeSettingsService, settingsFilter); + service.start(); + + HipChatAccount account = service.getAccount(accountName); + assertThat(account, notNullValue()); + assertThat(account.name, is(accountName)); + assertThat(account.authToken, is("_token")); + assertThat(account.profile, is(HipChatAccount.Profile.USER)); + assertThat(account.httpClient, is(httpClient)); + assertThat(account.server, notNullValue()); + assertThat(account.server.host(), is(host != null ? host : HipChatServer.DEFAULT.host())); + assertThat(account.server.port(), is(port > 0 ? port : HipChatServer.DEFAULT.port())); + assertThat(account, instanceOf(UserAccount.class)); + if (defaultRoom == null) { + assertThat(((UserAccount) account).defaults.rooms, nullValue()); + } else { + assertThat(((UserAccount) account).defaults.rooms, arrayContaining("_r1", "_r2")); + } + if (defaultUser == null) { + assertThat(((UserAccount) account).defaults.users, nullValue()); + } else { + assertThat(((UserAccount) account).defaults.users, arrayContaining("_u1", "_u2")); + } + assertThat(((UserAccount) account).defaults.color, is(defaultColor)); + assertThat(((UserAccount) account).defaults.format, is(defaultFormat)); + assertThat(((UserAccount) account).defaults.notify, is(defaultNotify)); + + // with a single account defined, making sure that that account is set to the default one. + assertThat(service.getDefaultAccount(), sameInstance(account)); + + assertThatSettingsFilterWasAdded(); + } + + @Test + public void testMultipleAccounts() throws Exception { + HipChatMessage.Color defaultColor = randomBoolean() ? null : randomFrom(HipChatMessage.Color.values()); + HipChatMessage.Format defaultFormat = randomBoolean() ? null : randomFrom(HipChatMessage.Format.values()); + Boolean defaultNotify = randomBoolean() ? null : (Boolean) randomBoolean(); + Settings.Builder settingsBuilder = Settings.builder(); + String defaultAccount = "_a" + randomIntBetween(0, 4); + settingsBuilder.put("watcher.actions.hipchat.service.default_account", defaultAccount); + + boolean customGlobalServer = randomBoolean(); + if (customGlobalServer) { + settingsBuilder.put("watcher.actions.hipchat.service.host", "_host_global"); + settingsBuilder.put("watcher.actions.hipchat.service.port", 299); + } + + for (int i = 0; i < 5; i++) { + String name = "_a" + i; + String prefix = "watcher.actions.hipchat.service.account." + name; + HipChatAccount.Profile profile = randomFrom(HipChatAccount.Profile.values()); + settingsBuilder.put(prefix + ".profile", profile); + settingsBuilder.put(prefix + ".auth_token", "_token" + i); + if (profile == HipChatAccount.Profile.INTEGRATION) { + settingsBuilder.put(prefix + ".room", "_room" + i); + } + if (i % 2 == 0) { + settingsBuilder.put(prefix + ".host", "_host" + i); + settingsBuilder.put(prefix + ".port", 300 + i); + } + buildMessageDefaults(name, settingsBuilder, null, null, null, defaultColor, defaultFormat, defaultNotify); + } + + InternalHipChatService service = new InternalHipChatService(settingsBuilder.build(), httpClient, nodeSettingsService, settingsFilter); + service.start(); + + for (int i = 0; i < 5; i++) { + String name = "_a" + i; + HipChatAccount account = service.getAccount(name); + assertThat(account, notNullValue()); + assertThat(account.name, is(name)); + assertThat(account.authToken, is("_token" + i)); + assertThat(account.profile, notNullValue()); + if (account.profile == HipChatAccount.Profile.INTEGRATION) { + assertThat(account, instanceOf(IntegrationAccount.class)); + assertThat(((IntegrationAccount) account).room, is("_room" + i)); + } + assertThat(account.httpClient, is(httpClient)); + assertThat(account.server, notNullValue()); + if (i % 2 == 0) { + assertThat(account.server.host(), is("_host" + i)); + assertThat(account.server.port(), is(300 + i)); + } else if (customGlobalServer) { + assertThat(account.server.host(), is("_host_global")); + assertThat(account.server.port(), is(299)); + } else { + assertThat(account.server.host(), is(HipChatServer.DEFAULT.host())); + assertThat(account.server.port(), is(HipChatServer.DEFAULT.port())); + } + } + + assertThat(service.getDefaultAccount(), sameInstance(service.getAccount(defaultAccount))); + + assertThatSettingsFilterWasAdded(); + } + + private void assertThatSettingsFilterWasAdded() { + verify(settingsFilter, times(1)).filterOut("watcher.actions.hipchat.service.account.*.auth_token"); + } + + private void buildMessageDefaults(String account, Settings.Builder settingsBuilder, String room, String user, String from, HipChatMessage.Color color, HipChatMessage.Format format, Boolean notify) { + if (room != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.room", room); + } + if (user != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.user", user); + } + if (from != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.from", from); + } + if (color != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.color", color.value()); + } + if (format != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.format", format); + } + if (notify != null) { + settingsBuilder.put("watcher.actions.hipchat.service.account." + account + ".message_defaults.notify", notify); + } + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccountTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccountTests.java new file mode 100644 index 00000000000..8123e4da9eb --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/UserAccountTests.java @@ -0,0 +1,239 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.support.http.*; +import org.junit.Test; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + +/** + * + */ +public class UserAccountTests extends ESTestCase { + + @Test + public void testSettings() throws Exception { + String accountName = "_name"; + + Settings.Builder sb = Settings.builder(); + + String authToken = randomAsciiOfLength(50); + sb.put(UserAccount.AUTH_TOKEN_SETTING, authToken); + + String host = HipChatServer.DEFAULT.host(); + if (randomBoolean()) { + host = randomAsciiOfLength(10); + sb.put(HipChatServer.HOST_SETTING, host); + } + int port = HipChatServer.DEFAULT.port(); + if (randomBoolean()) { + port = randomIntBetween(300, 400); + sb.put(HipChatServer.PORT_SETTING, port); + } + + String[] defaultRooms = null; + if (randomBoolean()) { + defaultRooms = new String[] { "_r1", "_r2" }; + sb.put(HipChatAccount.DEFAULT_ROOM_SETTING, "_r1,_r2"); + } + String[] defaultUsers = null; + if (randomBoolean()) { + defaultUsers = new String[] { "_u1", "_u2" }; + sb.put(HipChatAccount.DEFAULT_USER_SETTING, "_u1,_u2"); + } + HipChatMessage.Format defaultFormat = null; + if (randomBoolean()) { + defaultFormat = randomFrom(HipChatMessage.Format.values()); + sb.put(HipChatAccount.DEFAULT_FORMAT_SETTING, defaultFormat); + } + HipChatMessage.Color defaultColor = null; + if (randomBoolean()) { + defaultColor = randomFrom(HipChatMessage.Color.values()); + sb.put(HipChatAccount.DEFAULT_COLOR_SETTING, defaultColor); + } + Boolean defaultNotify = null; + if (randomBoolean()) { + defaultNotify = randomBoolean(); + sb.put(HipChatAccount.DEFAULT_NOTIFY_SETTING, defaultNotify); + } + Settings settings = sb.build(); + + UserAccount account = new UserAccount(accountName, settings, HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + + assertThat(account.profile, is(HipChatAccount.Profile.USER)); + assertThat(account.name, equalTo(accountName)); + assertThat(account.server.host(), is(host)); + assertThat(account.server.port(), is(port)); + assertThat(account.authToken, is(authToken)); + if (defaultRooms != null) { + assertThat(account.defaults.rooms, arrayContaining(defaultRooms)); + } else { + assertThat(account.defaults.rooms, nullValue()); + } + if (defaultUsers != null) { + assertThat(account.defaults.users, arrayContaining(defaultUsers)); + } else { + assertThat(account.defaults.users, nullValue()); + } + assertThat(account.defaults.format, is(defaultFormat)); + assertThat(account.defaults.color, is(defaultColor)); + assertThat(account.defaults.notify, is(defaultNotify)); + } + + @Test(expected = SettingsException.class) + public void testSettings_NoAuthToken() throws Exception { + Settings.Builder sb = Settings.builder(); + new UserAccount("_name", sb.build(), HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + } + + @Test + public void testSend() throws Exception { + HttpClient httpClient = mock(HttpClient.class); + UserAccount account = new UserAccount("_name", Settings.builder() + .put("host", "_host") + .put("port", "443") + .put("auth_token", "_token") + .build(), HipChatServer.DEFAULT, httpClient, mock(ESLogger.class)); + + HipChatMessage.Format format = randomFrom(HipChatMessage.Format.values()); + HipChatMessage.Color color = randomFrom(HipChatMessage.Color.values()); + Boolean notify = randomBoolean(); + final HipChatMessage message = new HipChatMessage("_body", new String[] { "_r1", "_r2" }, new String[] { "_u1", "_u2" }, null, format, color, notify); + + HttpRequest reqR1 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/room/_r1/notification") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer _token") + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("message", message.body); + if (message.format != null) { + builder.field("message_format", message.format.value()); + } + if (message.notify != null) { + builder.field("notify", message.notify); + } + if (message.color != null) { + builder.field("color", String.valueOf(message.color.value())); + } + return builder; + } + })) + .build(); + + logger.info("expected (r1): " + jsonBuilder().value(reqR1).bytes().toUtf8()); + + HttpResponse resR1 = mock(HttpResponse.class); + when(resR1.status()).thenReturn(200); + when(httpClient.execute(reqR1)).thenReturn(resR1); + + HttpRequest reqR2 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/room/_r2/notification") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer _token") + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("message", message.body); + if (message.format != null) { + builder.field("message_format", message.format.value()); + } + if (message.notify != null) { + builder.field("notify", message.notify); + } + if (message.color != null) { + builder.field("color", String.valueOf(message.color.value())); + } + return builder; + } + })) + .build(); + + logger.info("expected (r2): " + jsonBuilder().value(reqR1).bytes().toUtf8()); + + HttpResponse resR2 = mock(HttpResponse.class); + when(resR2.status()).thenReturn(200); + when(httpClient.execute(reqR2)).thenReturn(resR2); + + HttpRequest reqU1 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/user/_u1/message") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer _token") + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("message", message.body); + if (message.format != null) { + builder.field("message_format", message.format.value()); + } + if (message.notify != null) { + builder.field("notify", message.notify); + } + return builder; + } + })) + .build(); + + logger.info("expected (u1): " + jsonBuilder().value(reqU1).bytes().toUtf8()); + + HttpResponse resU1 = mock(HttpResponse.class); + when(resU1.status()).thenReturn(200); + when(httpClient.execute(reqU1)).thenReturn(resU1); + + HttpRequest reqU2 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v2/user/_u2/message") + .setHeader("Content-Type", "application/json") + .setHeader("Authorization", "Bearer _token") + .body(XContentHelper.toString(new ToXContent() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("message", message.body); + if (message.format != null) { + builder.field("message_format", message.format.value()); + } + if (message.notify != null) { + builder.field("notify", message.notify); + } + return builder; + } + })) + .build(); + + logger.info("expected (u2): " + jsonBuilder().value(reqU2).bytes().toUtf8()); + + HttpResponse resU2 = mock(HttpResponse.class); + when(resU2.status()).thenReturn(200); + when(httpClient.execute(reqU2)).thenReturn(resU2); + + account.send(message); + + verify(httpClient).execute(reqR1); + verify(httpClient).execute(reqR2); + verify(httpClient).execute(reqU2); + verify(httpClient).execute(reqU2); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/V1AccountTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/V1AccountTests.java new file mode 100644 index 00000000000..18b3813b7b2 --- /dev/null +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/hipchat/service/V1AccountTests.java @@ -0,0 +1,160 @@ +/* + * 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.watcher.actions.hipchat.service; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.support.http.*; +import org.junit.Test; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + +/** + * + */ +public class V1AccountTests extends ESTestCase { + + @Test + public void testSettings() throws Exception { + String accountName = "_name"; + + Settings.Builder sb = Settings.builder(); + + String authToken = randomAsciiOfLength(50); + sb.put(V1Account.AUTH_TOKEN_SETTING, authToken); + + String host = HipChatServer.DEFAULT.host(); + if (randomBoolean()) { + host = randomAsciiOfLength(10); + sb.put(HipChatServer.HOST_SETTING, host); + } + int port = HipChatServer.DEFAULT.port(); + if (randomBoolean()) { + port = randomIntBetween(300, 400); + sb.put(HipChatServer.PORT_SETTING, port); + } + + String[] defaultRooms = null; + if (randomBoolean()) { + defaultRooms = new String[] { "_r1", "_r2" }; + sb.put(HipChatAccount.DEFAULT_ROOM_SETTING, "_r1,_r2"); + } + String defaultFrom = null; + if (randomBoolean()) { + defaultFrom = randomAsciiOfLength(10); + sb.put(HipChatAccount.DEFAULT_FROM_SETTING, defaultFrom); + } + HipChatMessage.Format defaultFormat = null; + if (randomBoolean()) { + defaultFormat = randomFrom(HipChatMessage.Format.values()); + sb.put(HipChatAccount.DEFAULT_FORMAT_SETTING, defaultFormat); + } + HipChatMessage.Color defaultColor = null; + if (randomBoolean()) { + defaultColor = randomFrom(HipChatMessage.Color.values()); + sb.put(HipChatAccount.DEFAULT_COLOR_SETTING, defaultColor); + } + Boolean defaultNotify = null; + if (randomBoolean()) { + defaultNotify = randomBoolean(); + sb.put(HipChatAccount.DEFAULT_NOTIFY_SETTING, defaultNotify); + } + Settings settings = sb.build(); + + V1Account account = new V1Account(accountName, settings, HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + + assertThat(account.profile, is(HipChatAccount.Profile.V1)); + assertThat(account.name, equalTo(accountName)); + assertThat(account.server.host(), is(host)); + assertThat(account.server.port(), is(port)); + assertThat(account.authToken, is(authToken)); + if (defaultRooms != null) { + assertThat(account.defaults.rooms, arrayContaining(defaultRooms)); + } else { + assertThat(account.defaults.rooms, nullValue()); + } + assertThat(account.defaults.from, is(defaultFrom)); + assertThat(account.defaults.format, is(defaultFormat)); + assertThat(account.defaults.color, is(defaultColor)); + assertThat(account.defaults.notify, is(defaultNotify)); + } + + @Test(expected = SettingsException.class) + public void testSettings_NoAuthToken() throws Exception { + Settings.Builder sb = Settings.builder(); + new V1Account("_name", sb.build(), HipChatServer.DEFAULT, mock(HttpClient.class), mock(ESLogger.class)); + } + + @Test + public void testSend() throws Exception { + HttpClient httpClient = mock(HttpClient.class); + V1Account account = new V1Account("_name", Settings.builder() + .put("host", "_host") + .put("port", "443") + .put("auth_token", "_token") + .build(), HipChatServer.DEFAULT, httpClient, mock(ESLogger.class)); + + HipChatMessage.Format format = randomFrom(HipChatMessage.Format.values()); + HipChatMessage.Color color = randomFrom(HipChatMessage.Color.values()); + Boolean notify = randomBoolean(); + HipChatMessage message = new HipChatMessage("_body", new String[] { "_r1", "_r2" }, null, "_from", format, color, notify); + + HttpRequest req1 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v1/rooms/message") + .setHeader("Content-Type", "application/x-www-form-urlencoded") + .setParam("format", "json") + .setParam("auth_token", "_token") + .body(new StringBuilder() + .append("room_id=").append("_r1&") + .append("from=").append("_from&") + .append("message=").append("_body&") + .append("message_format=").append(format.value()).append("&") + .append("color=").append(color.value()).append("&") + .append("notify=").append(notify ? "1" : "0") + .toString()) + .build(); + + logger.info("expected (r1): " + jsonBuilder().value(req1).bytes().toUtf8()); + + HttpResponse res1 = mock(HttpResponse.class); + when(res1.status()).thenReturn(200); + when(httpClient.execute(req1)).thenReturn(res1); + + HttpRequest req2 = HttpRequest.builder("_host", 443) + .method(HttpMethod.POST) + .scheme(Scheme.HTTPS) + .path("/v1/rooms/message") + .setHeader("Content-Type", "application/x-www-form-urlencoded") + .setParam("format", "json") + .setParam("auth_token", "_token") + .body(new StringBuilder() + .append("room_id=").append("_r2&") + .append("from=").append("_from&") + .append("message=").append("_body&") + .append("message_format=").append(format.value()).append("&") + .append("color=").append(color.value()).append("&") + .append("notify=").append(notify ? "1" : "0") + .toString()) + .build(); + + logger.info("expected (r2): " + jsonBuilder().value(req2).bytes().toUtf8()); + + HttpResponse res2 = mock(HttpResponse.class); + when(res2.status()).thenReturn(200); + when(httpClient.execute(req2)).thenReturn(res2); + + account.send(message); + + verify(httpClient).execute(req1); + verify(httpClient).execute(req2); + } +} diff --git a/watcher/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java b/watcher/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java index 0a4753fad5b..e93a904f724 100644 --- a/watcher/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java +++ b/watcher/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.sort.SortOrder; diff --git a/watcher/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java b/watcher/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java index b9c11e48a7e..a6e64141045 100644 --- a/watcher/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java +++ b/watcher/src/test/java/org/elasticsearch/watcher/test/AbstractWatcherIntegrationTests.java @@ -327,6 +327,7 @@ public abstract class AbstractWatcherIntegrationTests extends ESIntegTestCase { assertWatchWithMinimumPerformedActionsCount(watchName, minimumExpectedWatchActionsWithActionPerformed, true); } + // TODO remove this shitty method... the `assertConditionMet` is bogus protected void assertWatchWithMinimumPerformedActionsCount(final String watchName, final long minimumExpectedWatchActionsWithActionPerformed, final boolean assertConditionMet) throws Exception { final AtomicReference lastResponse = new AtomicReference<>(); try { diff --git a/watcher/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java b/watcher/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java index d465172e678..c3a2e8829a7 100644 --- a/watcher/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java +++ b/watcher/src/test/java/org/elasticsearch/watcher/test/integration/BasicWatcherTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.watcher.test.integration; -import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRequestBuilder;