Skip to content

Commit

Permalink
Initial version (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinwyszynski authored Mar 29, 2023
1 parent 29dce92 commit 9ab0d67
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ override.tf.json

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

assets/input*
6 changes: 6 additions & 0 deletions .spacelift/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 1
module_version: 0.1.0

tests:
- name: Default test
project_root: ".spacelift/test"
20 changes: 20 additions & 0 deletions .spacelift/test/test.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
terraform {
required_providers {
spacelift = {
source = "spacelift-io/spacelift"
}
}
}


provider "spacelift" {}

variable "spacelift_run_id" {}

module "msteams-integration" {
source = "../../"

channel_name = var.spacelift_run_id
space_id = "public-modules-01GVNH2CJKSKHRSMDPBMQ3WZT9"
webhook_url = "https://devnull-as-a-service.com/dev/null"
}
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
# terraform-spacelift-msteams
Policy-based integration between Spacelift and Microsoft Teams
# Spacelift-Microsoft Teams notification integration

Terraform module providing a notification-based integration between [Spacelift](https://spacelift.io) and [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-teams/group-chat-software). It creates a webhook in Spacelift that will send notifications to a Microsoft Teams channel when:

- a [tracked run](https://docs.spacelift.io/concepts/run/tracked) [needs confirmation](https://docs.spacelift.io/concepts/run/tracked#unconfirmed);
- a [tracked run](https://docs.spacelift.io/concepts/run/tracked) or a [task](https://docs.spacelift.io/concepts/run/task) finishes;
- a [module version](https://docs.spacelift.io/vendors/terraform/module-registry#versions) succeeds or fails;

The official documentation for this integration is available in [here](https://docs.spacelift.io/integrations/chatops/msteams).

## Usage

```hcl
module "spacelift_msteams" {
source = "spacelift-io/msteams/spacelift"
channel_name = "My channel"
space_id = "root"
webhook_url = "https://outlook.office.com/webhook/..."
}
```

Based on this configuration, the module will send notifications to the `My channel` channel in Microsoft Teams that look like these:

![Run notification](https://docs.spacelift.io/assets/screenshots/msteams-run-state.png)

![Version notification](https://docs.spacelift.io/assets/screenshots/msteams-module-version.png)

## Prerequisites

In order to use this module, you need to have a Microsoft Teams channel and an incoming webhook URL. This needs to be set up manually.

To learn how to set up the webhook in Microsoft Teams, please refer to the [official documentation](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook).

## Further work

Microsoft Teams has a rich API for creating beautiful-looking, powerful integrations. Unfortunately, much of that is either not documented, and some is not available to incoming webhooks. If you're a heavy user of both Spacelift and Microsoft Teams, and familiar with the latter's API, we'd love to hear from you. Let's build something amazing together!
217 changes: 217 additions & 0 deletions assets/policy.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package spacelift

# Common settings.
fail_color = "e21717"
success_color = "17bd81"
warning_color = "f2a40b"

# START run state changes.
stack_url = sprintf("https://%s.spacelift.io/stack/%s", [
input.account.name,
input.run_updated.stack.id,
])

run_url = sprintf("%s/run/%s", [
stack_url,
input.run_updated.run.id,
])

interesting_run_states = {
"DISCARDED": { "color": fail_color, "title": "has been discarded" },
"FAILED": { "color": fail_color, "title": "has failed" },
"FINISHED": { "color": success_color, "title": "has finished" },
"STOPPED": { "color": fail_color, "title": "has been stopped" },
"UNCONFIRMED": { "color": warning_color, "title": "is pending confirmation" },
}

interesting_run_types = {
"TRACKED": "Tracked run",
"TASK": "Custom task",
}

run_state := input.run_updated.run.state
run_type := input.run_updated.run.type

# Run state changes.
webhook[{"endpoint_id": endpoint_id, "payload": run_payload}] {
# Send the webhook to any endpoint labeled as "msteams".
endpoint := input.webhook_endpoints[_]
endpoint.labels[_] == "msteams"
endpoint_id := endpoint.id

# Only send the webhook if both the run state and type are interesting.
interesting_run_states[run_state]
interesting_run_types[run_type]
}

# Best reference for the payload schema.
# https://adaptivecards.io/samples/
run_payload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": interesting_run_states[run_state].color,
"summary": sprintf("%s %s", [
interesting_run_types[run_type],
interesting_run_states[run_state].title],
),
"sections": run_sections,
}

# Main section.
run_sections[{
"activityTitle": sprintf("[%s](%s) %s", [
interesting_run_types[run_type],
run_url,
interesting_run_states[run_state].title,
]),
"activitySubtitle": sprintf("Stack [%s](%s)", [
input.run_updated.stack.name,
stack_url,
]),
"facts": run_facts,
"markdown": true
}]

run_sections[{
"activityTitle": sprintf("%s resources (%d)", [
resource_section,
count(collection),
]),
"text": as_html_list(collection)
}] {
run_type == "TRACKED"
resource_section := {"Added","Changed","Deleted","Replaced"}[_]
collection := resources(lower(resource_section))
count(collection) > 0
}

run_facts[{ "name": "Command", "value": value }] {
run_type == "TASK"
value := sprintf("`%s`", [input.run_updated.run.command])
}

run_facts[{ "name": "Branch", "value": value }] {
run_type != "TASK"
value := input.run_updated.run.commit.branch
}

run_facts[{ "name": "Commit", "value": value }] {
run_type != "TASK"
value := sprintf("[%s](%s) by %s", [
input.run_updated.run.commit.message,
input.run_updated.run.commit.hash,
input.run_updated.run.commit.author,
])
}

run_facts[{ "name": "Triggered by", "value": value }] {
value := input.run_updated.run.triggered_by
}

run_facts[{ "name": "Created at", "value": value }] {
value := time.format(input.run_updated.run.created_at)
}

run_facts[{ "name": "Space", "value": value }] {
value := input.run_updated.stack.space.name
}

resources(type) = [change.entity.address |
change := input.run_updated.run.changes[_]
change.phase == "plan"
contains(change.action, type)
]

as_html_list(resources) = sprintf("<ul>%s</ul>", [
concat("", [sprintf("<li>%s</li>", [resource]) | resource := resources[_]]),
])
# END run state changes.


# START module version state changes.
module_url := sprintf("https://%s.spacelift.io/module/%s", [
input.account.name,
input.module_version.module.id,
])

version_number := input.module_version.version.number
version_state := input.module_version.version.state
version_url := sprintf("%s/version/%s", [
module_url,
input.module_version.version.id,
])

interesting_version_states = {
"ACTIVE": { "color": success_color, "title": "has been published" },
"FAILED": { "color": fail_color, "title": "has failed" },
}

# Module version state changes.
webhook[{"endpoint_id": endpoint_id, "payload": version_payload}] {
# Send the webhook to any endpoint labeled as "msteams".
endpoint := input.webhook_endpoints[_]
endpoint.labels[_] == "msteams"
endpoint_id := endpoint.id

# Only send the webhook if the version state is interesting.
interesting_version_states[version_state]
}

version_payload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": interesting_version_states[version_state].color,
"summary": sprintf("Module version %s %s", [
version_number,
interesting_version_states[version_state].title,
]),
"sections": version_sections,
}

version_sections[{
"activityTitle": sprintf("[Module version %s](%s) %s", [
version_number,
version_url,
interesting_version_states[version_state].title,
]),
"activitySubtitle": sprintf("Module [%s](%s)", [
input.module_version.module.name,
module_url,
]),
"facts": version_facts,
"markdown": true
}]

version_facts[{ "name": "Branch", "value": value }] {
value := input.module_version.version.commit.branch
}

version_facts[{ "name": "Commit", "value": value }] {
value := sprintf("[%s](%s) by %s", [
input.module_version.version.commit.message,
input.module_version.version.commit.hash,
input.module_version.version.commit.author,
])
}

version_facts[{ "name": "Created at", "value": value }] {
value := time.format(input.module_version.version.created_at)
}

version_facts[{ "name": "Space", "value": value }] {
value := input.module_version.module.space.name
}

version_facts[{ "name": name, "value": value }] {
some i
run := input.module_version.version.test_runs[i]

name := sprintf("Test case #%02d: %q", [i + 1, run.title])
value := sprintf("[%s](%s)", [
run.state,
sprintf("%s/run/%s", [version_url, run.id]),
])
}

# Sample if we actually send a webhook.
sample { count(webhook) > 0 }
8 changes: 8 additions & 0 deletions policy.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "spacelift_policy" "msteams-integration" {
name = "MS Teams Integration (${var.channel_name})"
type = "NOTIFICATION"
space_id = var.space_id

body = file("${path.module}/assets/policy.rego")
labels = ["msteams"]
}
8 changes: 8 additions & 0 deletions providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
spacelift = {
source = "spacelift-io/spacelift"
version = ">= 1.1.4"
}
}
}
16 changes: 16 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
variable "channel_name" {
type = string
description = "MS Teams channel name to send notifications to. This is just used for naming purposes."
}

variable "space_id" {
type = string
description = "ID of the Spacelift space to create notitications for."
default = "root"
}

variable "webhook_url" {
type = string
description = "URL of the MS Teams channel incoming webhook connector to send notifications to."
sensitive = true
}
9 changes: 9 additions & 0 deletions webhook.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "spacelift_named_webhook" "msteams-integration" {
name = "MS Teams Integration (${var.channel_name})"
space_id = var.space_id

endpoint = var.webhook_url
enabled = true

labels = ["msteams"]
}

0 comments on commit 9ab0d67

Please sign in to comment.