Skip to content

Commit

Permalink
feat: Add a Zulip subplugin (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
hhovhann authored Aug 26, 2024
1 parent 265475a commit ecfc3c4
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.kestra.plugin.notifications.zulip;

import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.tasks.VoidOutput;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.notifications.ExecutionInterface;
import io.kestra.plugin.notifications.services.ExecutionService;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import java.util.Map;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Send a Zulip message with the execution information",
description = "The message will include a link to the execution page in the UI along with the execution ID, namespace, flow name, the start date, duration and the final status of the execution, and (if failed) the task that led to a failure.\n\n" +
"Use this notification task only in a flow that has a [Flow trigger](https://kestra.io/docs/administrator-guide/monitoring#alerting). Don't use this notification task in `errors` tasks. Instead, for `errors` tasks, use the [ZulipIncomingWebhook](https://kestra.io/plugins/plugin-notifications/tasks/slack/io.kestra.plugin.notifications.slack.slackincomingwebhook) task."
)
@Plugin(
examples = {
@Example(
title = "Send a Zulip notification on a failed flow execution",
full = true,
code = """
id: failure_alert
namespace: company.team
tasks:
- id: send_alert
type: io.kestra.plugin.notifications.zulip.ZulipExecution
url: "{{ secret('ZULIP_WEBHOOK') }}" # format: https://yourZulipDomain.zulipchat.com/api/v1/external/INTEGRATION_NAME?api_key=API_KEY
channel: "#general"
executionId: "{{trigger.executionId}}"
triggers:
- id: failed_prod_workflows
type: io.kestra.plugin.core.trigger.Flow
conditions:
- type: io.kestra.plugin.core.condition.ExecutionStatusCondition
in:
- FAILED
- WARNING
- type: io.kestra.plugin.core.condition.ExecutionNamespaceCondition
namespace: prod
prefix: true
"""
)
}
)
public class ZulipExecution extends ZulipTemplate implements ExecutionInterface {
@Builder.Default
private final String executionId = "{{ execution.id }}";
private Map<String, Object> customFields;
private String customMessage;

@Override
public VoidOutput run(RunContext runContext) throws Exception {
this.templateUri = "zulip-template.peb";
this.templateRenderMap = ExecutionService.executionMap(runContext, this);

return super.run(runContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package io.kestra.plugin.notifications.zulip;

import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.tasks.VoidOutput;
import io.kestra.core.runners.RunContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.netty.DefaultHttpClient;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import java.net.URI;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Send a Zulip message using an Incoming Webhook",
description = "Add this task to send direct Zulip notifications. Check the <a href=\"https://api.zulip.com/messaging/webhooks\">Zulip documentation</a> for more details.."
)
@Plugin(
examples = {
@Example(
title = "Send a Zulip notification on a failed flow execution",
full = true,
code = """
id: unreliable_flow
namespace: company.team
tasks:
- id: fail
type: io.kestra.plugin.scripts.shell.Commands
runner: PROCESS
commands:
- exit 1
errors:
- id: alert_on_failure
type: io.kestra.plugin.notifications.zulip.ZulipIncomingWebhook
url: "{{ secret('ZULIP_WEBHOOK') }}" # https://yourZulipDomain.zulipchat.com/api/v1/external/INTEGRATION_NAME?api_key=API_KEY
payload: |
{
"text": "Failure alert for flow {{ flow.namespace }}.{{ flow.id }} with ID {{ execution.id }}"
}
"""
),
@Example(
title = "Send a Zulip message via incoming webhook with a text argument",
full = true,
code = """
id: zulip_incoming_webhook
namespace: company.team
tasks:
- id: send_zulip_message
type: io.kestra.plugin.notifications.zulip.ZulipIncomingWebhook
url: "{{ secret('ZULIP_WEBHOOK') }}" # https://yourZulipDomain.zulipchat.com/api/v1/external/INTEGRATION_NAME?api_key=API_KEY
payload: |
{
"text": "Hello from the workflow {{ flow.id }}"
}
"""
),
@Example(
title = "Send a Zulip message via incoming webhook with a blocks argument, read more on blocks <a href=\"https://api.zulip.com/reference/block-kit/blocks\">here</a>",
full = true,
code = """
id: zulip_incoming_webhook
namespace: company.team
tasks:
- id: send_zulip_message
type: io.kestra.plugin.notifications.zulip.ZulipIncomingWebhook
url: "{{ secret('ZULIP_WEBHOOK') }}" # format: https://yourZulipDomain.zulipchat.com/api/v1/external/INTEGRATION_NAME?api_key=API_KEY
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Hello from the workflow *{{ flow.id }}*"
}
}
]
}
"""
),
}
)
public class ZulipIncomingWebhook extends Task implements RunnableTask<VoidOutput> {
@Schema(
title = "Zulip incoming webhook URL",
description = "Check the <a href=\"https://zulip.com/api/incoming-webhooks-overview\">Incoming Webhook Integrations</a> documentation for more details.."
)
@PluginProperty(dynamic = true)
@NotEmpty
private String url;

@Schema(
title = "Zulip message payload"
)
@PluginProperty(dynamic = true)
protected String payload;

@Override
public VoidOutput run(RunContext runContext) throws Exception {
String url = runContext.render(this.url);

try (DefaultHttpClient client = new DefaultHttpClient(URI.create(url))) {
String payload = runContext.render(this.payload);

runContext.logger().debug("Send Zulip webhook: {}", payload);

client.toBlocking().retrieve(HttpRequest.POST(url, payload));
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.kestra.plugin.notifications.zulip;

import com.google.common.base.Charsets;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.tasks.VoidOutput;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.JacksonMapper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.apache.commons.io.IOUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
public abstract class ZulipTemplate extends ZulipIncomingWebhook {
@Schema(
title = "Zulip channel to send the message to"
)
@PluginProperty(dynamic = true)
protected String channel;

@Schema(
title = "Author of the zulip message"
)
@PluginProperty(dynamic = true)
protected String username;

@Schema(
title = "Url of the icon to use"
)
@PluginProperty(dynamic = true)
protected String iconUrl;

@Schema(
title = "Emoji icon to use"
)
@PluginProperty(dynamic = true)
protected String iconEmoji;

@Schema(
title = "Template to use",
hidden = true
)
@PluginProperty(dynamic = true)
protected String templateUri;

@Schema(
title = "Map of variables to use for the message template"
)
@PluginProperty(dynamic = true)
protected Map<String, Object> templateRenderMap;


@SuppressWarnings("unchecked")
@Override
public VoidOutput run(RunContext runContext) throws Exception {
Map<String, Object> map = new HashMap<>();

if (this.templateUri != null) {
String template = IOUtils.toString(
Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream(this.templateUri)),
Charsets.UTF_8
);

String render = runContext.render(template, templateRenderMap != null ? templateRenderMap : Map.of());
map = (Map<String, Object>) JacksonMapper.ofJson().readValue(render, Object.class);
}

if (this.channel != null) {
map.put("channel", runContext.render(this.channel));
}

if (this.username != null) {
map.put("username", runContext.render(this.username));
}

if (this.iconUrl != null) {
map.put("icon_url", runContext.render(this.iconUrl));
}

if (this.iconEmoji != null) {
map.put("icon_emoji", runContext.render(this.iconEmoji));
}

this.payload = JacksonMapper.ofJson().writeValueAsString(map);

return super.run(runContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@PluginSubGroup(
description = "This sub-group of plugins contains tasks for Zulip notifications.",
categories = PluginSubGroup.PluginCategory.ALERTING
)
package io.kestra.plugin.notifications.zulip;

import io.kestra.core.models.annotations.PluginSubGroup;
77 changes: 77 additions & 0 deletions src/main/resources/zulip-template.peb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{%- macro title(link, execution) -%}
*
{%- if link is defined -%}
<{{link}}|[{{execution.namespace}}] {{execution.flowId}} ➛ {{execution.state.current}}>
{%- else -%}
[{{execution.namespace}}] {{execution.flowId}} ➛ {{execution.state.current}}
{%- endif -%}
*
{%- endmacro -%}
{
"text": "{{ title(link, execution) }}\n> {% if firstFailed == false %}Succeeded{% else %}Failed on task `{{firstFailed.taskId}}`{% endif %} after {{duration}}",
"blocks": [
{% if customMessage is defined %}
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": {{ customMessage | json }}
}
},
{% endif %}
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{ title(link, execution) }}\n> {% if firstFailed == false %}Succeeded{% else %}Failed on task `{{firstFailed.taskId}}`{% endif %} after {{duration}}"
}
{% if link is defined %},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Details"
},
"url": "{{link}}"
}
{% endif %}
}
],
"attachments": [
{
"color": "{{ execution.state.current == "SUCCESS" ? 'good' : (execution.state.current == "WARNING" ? 'warning' : 'danger') }}",
"fields": [
{
"title": "Namespace",
"value": {{execution.namespace | json }},
"short": true
},
{
"title": "Flow ID",
"value": {{execution.flowId | json}},
"short": true
},
{
"title": "Execution ID",
"value": {{execution.id | json}},
"short": true
},
{
"title": "Execution Status",
"value": {{execution.state.current | json }},
"short": true
}
{% if customFields is defined %}
{% for entry in customFields %}
,{
"title": {{entry.key | json}},
"value": {{entry.value | json }},
"short": true
}
{% endfor %}
{% endif %}
],
"ts": {{ startDate | timestamp }}
}
]
}
Loading

0 comments on commit ecfc3c4

Please sign in to comment.