Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AS8-6579] Initial checkin #1

Merged
merged 8 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# ------------------------------------------------------------------------------
# Python
# ------------------------------------------------------------------------------

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/


# ------------------------------------------------------------------------------
# Terraform
# ------------------------------------------------------------------------------

# Local .terraform directories
**/.terraform/*
**/.terragrunt-cache/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log

# Ignore any .tfvars files that are generated automatically for each Terraform run. Most
# .tfvars files are managed as part of configuration and so should be included in
# version control.
#
# example.tfvars

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf

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

# macOS system files
**/.DS_Store

# Exclude IDE generated folders/files
.idea/
.vscode/
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
- repo: git://github.com/antonbabenko/pre-commit-terraform
rev: v1.50.0
hooks:
- id: terraform_fmt
- id: terraform_docs
args: ['-a --required=false']
- id: terraform_validate
96 changes: 95 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,96 @@
# cloud-build-slack-notifier
Terraform Module to add Slack notifications to Cloud Build

A Terraform module to enable Slack notifications for Cloud Build events.

**Note - This will add the following resources to your project:**

- Google Secret Manager for storing the Slack Webhook URL
- Google Cloud Storage Bucket for storing the notifier configuration
- Google Pub/Sub for events emitted from Cloud Build
- Google Cloud Run for processing the events emitted from Cloud Build
cjonesy marked this conversation as resolved.
Show resolved Hide resolved

This module is mostly based on instructions found in GCP's [Configuring Slack notifications](https://cloud.google.com/build/docs/configuring-notifications/configure-slack).

## Setup

You will need a Slack app incoming webhook url for this to work.

- Create a [Slack app](https://api.slack.com/apps?new_app=1) for your desired Slack workspace.
- Activate [incoming webhooks](https://api.slack.com/messaging/webhooks) to post messages from Cloud Build to Slack.
- After this module has run, add the webhook url to the secret in the UI

## Pre-commit Hooks

[Pre-commit](https://pre-commit.com/) hooks have been configured for this repo.

The enabled hooks check for a variety of common problems in Terraform code, and
will run any time you commit to your branch.

Pre-commit (and dependencies) can be installed by running:
`brew install pre-commit coreutils terraform-docs`

To enable the hooks locally, run the following from the root of this repo:
`pre-commit install`

To uninstall the hooks, run the following from the root of this repo:
`pre-commit uninstall`

To skip running the hooks when you commit:
`git commit -n` aka `git commit --no-verify`

**Currently enabled plugins:**

- [pre-commit-terraform](https://github.com/antonbabenko/pre-commit-terraform)
- `terraform_fmt`: Rewrites all Terraform configuration files to a canonical format
- `terraform_docs`: Inserts input and output documentation into `README.md`
- `terraform_validate`: Validates all Terraform configuration files
- [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks)
- `end-of-file-fixer`: Makes sure files end in a newline and only a newline
- `trailing-whitespace`: Trims trailing whitespace
- `check-merge-conflict`: Check for files that contain merge conflict strings

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements

No requirements.

## Providers

| Name | Version |
|------|---------|
| <a name="provider_archive"></a> [archive](#provider\_archive) | n/a |
| <a name="provider_google"></a> [google](#provider\_google) | n/a |
| <a name="provider_google-beta"></a> [google-beta](#provider\_google-beta) | n/a |
| <a name="provider_random"></a> [random](#provider\_random) | n/a |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [google-beta_google_cloudfunctions_function.slack_notifier](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_cloudfunctions_function) | resource |
| [google_project_service.apis](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource |
| [google_pubsub_topic.cloud_builds](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_topic) | resource |
| [google_secret_manager_secret.cloud_build_slack_webhook_url](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource |
| [google_secret_manager_secret_iam_member.slack_notifier_secret_accessor](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_iam_member) | resource |
| [google_service_account.slack_notifier](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource |
| [google_storage_bucket.cloud_build_notifier](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket) | resource |
| [google_storage_bucket_object.cloud_build_slack_notifier_script](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_object) | resource |
| [random_id.cloud_build_notifier](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
| [archive_file.slack_notifier](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |

## Inputs

| Name | Description | Type | Default |
|------|-------------|------|---------|
| <a name="input_cloud_build_event_filter"></a> [cloud\_build\_event\_filter](#input\_cloud\_build\_event\_filter) | The filter to apply to incoming Cloud Build events. | `string` | `"build.substitutions[\"BRANCH_NAME\"] == \"main\""` |
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | Project ID for the project in which Cloud Build is running. | `string` | n/a |
| <a name="input_slack_channel_name"></a> [slack\_channel\_name](#input\_slack\_channel\_name) | The Slack channel name in which to publish notifications. | `string` | n/a |

## Outputs

No outputs.
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
127 changes: 127 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Cloud Build Slack Notifier

# Enable required APIs
resource "google_project_service" "apis" {
for_each = toset([
# Ensure cloudbuild API is enabled (it should already be though)
"cloudbuild.googleapis.com",
# Cloud Functions is running our notifier
"cloudfunctions.googleapis.com",
# Pub/Sub is used to handle events from Cloud Build
"pubsub.googleapis.com",
# Slack webhook URL is stored in GSM for Cloud Run to use
cjonesy marked this conversation as resolved.
Show resolved Hide resolved
"secretmanager.googleapis.com"
])
project = var.project_id
service = each.key

disable_dependent_services = true
}


# ------------------------------------------------------------------------------
# Service Accounts
# ------------------------------------------------------------------------------

# Create the slack_notifier service account
resource "google_service_account" "slack_notifier" {
account_id = "slack-notifier"
project = var.project_id
}


# ------------------------------------------------------------------------------
# Secrets
# ------------------------------------------------------------------------------

# Setup a secret to store incoming slack webhook URL in Secret Manager
resource "google_secret_manager_secret" "cloud_build_slack_webhook_url" {
project = var.project_id
secret_id = "cloud-build-slack-webhook-url-${var.slack_channel_name}"

replication {
automatic = true
}
}
cjonesy marked this conversation as resolved.
Show resolved Hide resolved

# Give the slack notifier service account access to the secret
resource "google_secret_manager_secret_iam_member" "slack_notifier_secret_accessor" {
project = var.project_id
secret_id = google_secret_manager_secret.cloud_build_slack_webhook_url.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.slack_notifier.email}"
}


# ------------------------------------------------------------------------------
# Pub/Sub
# ------------------------------------------------------------------------------

# Create the cloud-builds topic to receive build update messages for your notifier
resource "google_pubsub_topic" "cloud_builds" {
project = var.project_id
name = "cloud-builds"
}


# ------------------------------------------------------------------------------
# GCS Bucket
# ------------------------------------------------------------------------------

# Create bucket
resource "random_id" "cloud_build_notifier" {
byte_length = 4
}

resource "google_storage_bucket" "cloud_build_notifier" {
project = var.project_id
name = "${var.project_id}-us-cloud-build-notifier-${random_id.cloud_build_notifier.hex}"
force_destroy = true
}

data "archive_file" "slack_notifier" {
type = "zip"
source_dir = "${path.module}/slack_notifier"
output_path = "${path.module}/slack_notifier.zip"
}

resource "google_storage_bucket_object" "cloud_build_slack_notifier_script" {
name = "slack_notifier.zip"
source = data.archive_file.slack_notifier.output_path
bucket = google_storage_bucket.cloud_build_notifier.name
}


# ------------------------------------------------------------------------------
# Cloud Function
# ------------------------------------------------------------------------------
resource "google_cloudfunctions_function" "slack_notifier" {
provider = google-beta
name = "slack-notifier-${regex("[0-9A-Za-z]+", google_storage_bucket_object.cloud_build_slack_notifier_script.crc32c)}" # HACK To make the function change when the script changes
description = "Slack Notifier"
runtime = "python39"
project = var.project_id
region = "us-central1" # Note: This can only be us-central1 according to docs

available_memory_mb = 128
source_archive_bucket = google_storage_bucket.cloud_build_notifier.name
source_archive_object = google_storage_bucket_object.cloud_build_slack_notifier_script.name

event_trigger {
event_type = "google.pubsub.topic.publish"
resource = "cloud-builds"
}

timeout = 60
entry_point = "handle_cloudbuild_event"

environment_variables = {
SECRET_ID = google_secret_manager_secret.cloud_build_slack_webhook_url.id
}

service_account_email = google_service_account.slack_notifier.email

depends_on = [
google_storage_bucket_object.cloud_build_slack_notifier_script
]
}
Empty file added outputs.tf
Empty file.
1 change: 1 addition & 0 deletions slack_notifier/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .cloud_build_event import CloudBuildEvent
56 changes: 56 additions & 0 deletions slack_notifier/events/cloud_build_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from google.cloud.devtools.cloudbuild import CloudBuildClient


class CloudBuildEvent:
def __init__(self, payload):
self._client = CloudBuildClient()
self.build_trigger = self._get_trigger(
payload.get("projectId"), payload.get("buildTriggerId")
)
self.substitutions = payload.get("substitutions", {})

self.id = payload.get("id")
self.project_id = payload.get("projectId")
self.status = payload.get("status")
self.log_url = payload.get("logUrl")
self.trigger_name = self.substitutions.get("TRIGGER_NAME")
self.repo_branch = self.substitutions.get("BRANCH_NAME")
self.commit_sha = self.substitutions.get("COMMIT_SHA")
self.short_sha = self.substitutions.get("SHORT_SHA")
self.pr_number = self.substitutions.get("_PR_NUMBER")

def _get_trigger(self, project_id, trigger_id):
trigger = self._client.get_build_trigger(
project_id=project_id, trigger_id=trigger_id
)
return trigger

@property
def github_repo(self):
if self.build_trigger.github is not None:
return f"{self.build_trigger.github.owner}/{self.build_trigger.github.name}"
return None

@property
def github_repo_url(self):
if self.github_repo is not None:
return f"https://github.com/{self.github_repo}"
return None

@property
def github_commit_url(self):
if self.github_repo_url is not None:
return f"{self.github_repo_url}/commit/{self.commit_sha}"
return None

@property
def github_pr_url(self):
if self.github_repo_url is not None and self.pr_number is not None:
return f"{self.github_repo_url}/pull/{self.pr_number}"
return None

@property
def github_branch_url(self):
if self.github_repo_url is not None:
return f"{self.github_repo_url}/tree/{self.repo_branch}"
return None
3 changes: 3 additions & 0 deletions slack_notifier/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
slack-sdk==3.5.1
google-cloud-secret-manager==2.4.0
google-cloud-build==3.2.1
Loading