Skip to content

Commit

Permalink
Merge pull request #41 from axiomhq/custom_webhook_and_createds
Browse files Browse the repository at this point in the history
feature(custom_webhook): added Monitor Type support, including Match Event & Custom Webhooks notifiers
  • Loading branch information
Rambatino authored Jul 26, 2024
2 parents 21df00d + 723b961 commit 3277383
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 89 deletions.
29 changes: 26 additions & 3 deletions axiom/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"

ax "github.com/axiomhq/axiom-go/axiom"
)
Expand Down Expand Up @@ -180,9 +181,11 @@ func testAccCheckResourcesCreatesCorrectValues(client *ax.Client, resourceName,
return fmt.Errorf("property %s not found in Terraform state", tfKey)
}

actualValue, found := actualMap[apiKey]
if !found {
return fmt.Errorf("property %s not found in API response", apiKey)
// Use gjson to get the value using the dot notation path
actualValue := gjson.GetBytes(actualJSON, apiKey)

if !actualValue.Exists() {
return fmt.Errorf("property %s not found in API response: %s", apiKey, string(actualJSON))
}

if fmt.Sprintf("%v", actualValue) != stateValue {
Expand Down Expand Up @@ -233,6 +236,26 @@ resource "axiom_monitor" "test_monitor" {
notify_by_group = false
}
resource "axiom_monitor" "test_monitor_match_event" {
depends_on = [axiom_dataset.test, axiom_notifier.slack_test]
name = "test event matching monitor"
description = "test_monitor updated"
apl_query = <<EOT
['terraform-provider-dataset']
| summarize count() by bin_auto(_time)
EOT
interval_minutes = 5
operator = "Above"
range_minutes = 5
threshold = 1
notifier_ids = [
axiom_notifier.slack_test.id
]
alert_on_no_data = false
notify_by_group = false
}
resource "axiom_token" "test_token" {
name = "test_token"
description = "test_token"
Expand Down
70 changes: 69 additions & 1 deletion axiom/resource_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type NotifierProperties struct {
Pagerduty *PagerDutyConfig `tfsdk:"pagerduty"`
Slack *SlackConfig `tfsdk:"slack"`
Webhook *WebhookConfig `tfsdk:"webhook"`
CustomWebhook *CustomWebhookConfig `tfsdk:"custom_webhook"`
}

type SlackConfig struct {
Expand All @@ -60,7 +61,7 @@ type DiscordConfig struct {
}

type DiscordWebhookConfig struct {
DiscordWebhookURL types.String `tfsdk:"Discord_webhook_url"`
DiscordWebhookURL types.String `tfsdk:"discord_webhook_url"`
}

type EmailConfig struct {
Expand All @@ -81,6 +82,12 @@ type WebhookConfig struct {
URL types.String `tfsdk:"url"`
}

type CustomWebhookConfig struct {
URL types.String `tfsdk:"url"`
Headers types.Map `tfsdk:"headers"`
Body types.String `tfsdk:"body"`
}

func (r *NotifierResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_notifier"
}
Expand Down Expand Up @@ -120,6 +127,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -143,6 +151,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -162,6 +171,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -182,6 +192,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -205,6 +216,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("email"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -228,6 +240,7 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("email"),
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("webhook"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
Expand All @@ -247,6 +260,37 @@ func (r *NotifierResource) Schema(_ context.Context, _ resource.SchemaRequest, r
path.MatchRelative().AtParent().AtName("email"),
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("custom_webhook"),
),
},
},
"custom_webhook": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"url": schema.StringAttribute{
MarkdownDescription: "The webhook URL",
Required: true,
},
"body": schema.StringAttribute{
MarkdownDescription: "The JSON body",
Required: true,
},
"headers": schema.MapAttribute{
ElementType: types.StringType,
MarkdownDescription: "Any headers associated with the request",
Optional: true,
Sensitive: true,
},
},
Optional: true,
Validators: []validator.Object{
objectvalidator.ExactlyOneOf(
path.MatchRelative().AtParent().AtName("slack"),
path.MatchRelative().AtParent().AtName("discord"),
path.MatchRelative().AtParent().AtName("discord_webhook"),
path.MatchRelative().AtParent().AtName("email"),
path.MatchRelative().AtParent().AtName("opsgenie"),
path.MatchRelative().AtParent().AtName("pagerduty"),
path.MatchRelative().AtParent().AtName("webhook"),
),
},
},
Expand Down Expand Up @@ -411,6 +455,17 @@ func extractNotifier(ctx context.Context, plan NotifierResourceModel) (*axiom.No
notifier.Properties.Webhook = &axiom.WebhookConfig{
URL: plan.Properties.Webhook.URL.ValueString(),
}
case plan.Properties.CustomWebhook != nil:
headers := map[string]string{}
diags := plan.Properties.CustomWebhook.Headers.ElementsAs(ctx, &headers, false)
if diags.HasError() {
return nil, diags
}
notifier.Properties.CustomWebhook = &axiom.CustomWebhook{
URL: plan.Properties.CustomWebhook.URL.ValueString(),
Headers: headers,
Body: plan.Properties.CustomWebhook.Body.ValueString(),
}
}

return &notifier, diags
Expand Down Expand Up @@ -464,6 +519,19 @@ func buildNotifierProperties(properties axiom.NotifierProperties) *NotifierPrope
URL: types.StringValue(properties.Webhook.URL),
}
}
if properties.CustomWebhook != nil {
headerValues := map[string]attr.Value{}
for k, v := range properties.CustomWebhook.Headers {
headerValues[k] = types.StringValue(v)
}
headers := types.MapValueMust(types.StringType, headerValues)

notifierProperties.CustomWebhook = &CustomWebhookConfig{
URL: types.StringValue(properties.CustomWebhook.URL),
Headers: headers,
Body: types.StringValue(properties.CustomWebhook.Body),
}
}
return &notifierProperties
}

Expand Down
141 changes: 141 additions & 0 deletions axiom/resource_notifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package axiom

import (
"os"
"testing"

ax "github.com/axiomhq/axiom-go/axiom"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/assert"
)

func TestNotifiers(t *testing.T) {
client, err := ax.NewClient()
assert.NoError(t, err)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"axiom": providerserver.NewProtocol6WithError(NewAxiomProvider()),
},
CheckDestroy: testAccCheckAxiomResourcesDestroyed(client),
Steps: []resource.TestStep{
{
Config: testConfigNotifier("slack", ` {
slack = {
slack_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.slack", "properties.slack.slack_url", "properties.slack.slackUrl"),
),
},
{
Config: testConfigNotifier("discord", ` {
discord = {
discord_channel = "general"
discord_token = "fake_discord_token"
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.discord", "properties.discord.discord_channel", "properties.discord.discordChannel"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.discord", "properties.discord.discord_token", "properties.discord.discordToken"),
),
},
{
Config: testConfigNotifier("discord_webhook", ` {
discord_webhook = {
discord_webhook_url = "https://discord.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz"
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.discord_webhook", "properties.discord_webhook.discord_webhook_url", "properties.discordWebhook.discordWebhookUrl"),
),
},
{
Config: testConfigNotifier("email", ` {
email = {
emails = ["test@example.com"]
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.email", "properties.email.emails.0", "properties.email.emails.0"),
),
},
{
Config: testConfigNotifier("opsgenie", ` {
opsgenie = {
api_key = "fake_opsgenie_api_key"
is_eu = true
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.opsgenie", "properties.opsgenie.api_key", "properties.opsgenie.apiKey"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.opsgenie", "properties.opsgenie.is_eu", "properties.opsgenie.isEU"),
),
},
{
Config: testConfigNotifier("pagerduty", ` {
pagerduty = {
routing_key = "fake_routing_key"
token = "fake_pagerduty_token"
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.pagerduty", "properties.pagerduty.routing_key", "properties.pagerduty.routingKey"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.pagerduty", "properties.pagerduty.token", "properties.pagerduty.token"),
),
},
{
Config: testConfigNotifier("webhook", ` {
webhook = {
url = "https://example.com/webhook"
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.webhook", "properties.webhook.url", "properties.webhook.url"),
),
},
{
Config: testConfigNotifier("custom_webhook", ` {
custom_webhook = {
url = "https://example.com/custom_webhook"
headers = {
"Authorization" = "Bearer token"
"Content-Type" = "application/json"
}
body = `+`<<EOF
{
"action": "{{.Action}}",
"event": {
"monitorID": "{{.MonitorID}}",
"body": "{{.Body}}",
"description": "{{.Description}}",
"queryEndTime": "{{.QueryEndTime}}",
"queryStartTime": "{{.QueryStartTime}}",
"timestamp": "{{.Timestamp}}",
"title": "{{.Title}}",
"value": {{.Value}},
"matchedEvent": {{jsonObject .MatchedEvent}}
}
}
EOF`+`
} }`),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.custom_webhook", "properties.custom_webhook.url", "properties.customWebhook.URL"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.custom_webhook", "properties.custom_webhook.headers.Authorization", "properties.customWebhook.Headers.Authorization"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.custom_webhook", "properties.custom_webhook.headers.Content-Type", "properties.customWebhook.Headers.Content-Type"),
testAccCheckResourcesCreatesCorrectValues(client, "axiom_notifier.custom_webhook", "properties.custom_webhook.body", "properties.customWebhook.Body"),
),
},
},
})
}

func testConfigNotifier(notifierType, notifierConfig string) string {
return `
provider "axiom" {
api_token = "` + os.Getenv("AXIOM_TOKEN") + `"
base_url = "` + os.Getenv("AXIOM_URL") + `"
}
resource "axiom_notifier" "` + notifierType + `" {
name = "` + notifierType + `"
properties = ` + notifierConfig + `
}
`
}
10 changes: 10 additions & 0 deletions docs/data-sources/notifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Read-Only:
- `pagerduty` (Attributes) (see [below for nested schema](#nestedatt--properties--pagerduty))
- `slack` (Attributes) (see [below for nested schema](#nestedatt--properties--slack))
- `webhook` (Attributes) (see [below for nested schema](#nestedatt--properties--webhook))
- `custom_webhook` (Attributes) (see [below for nested schema](#nestedatt--properties--custom_webhook))

<a id="nestedatt--properties--discord"></a>
### Nested Schema for `properties.discord`
Expand Down Expand Up @@ -94,3 +95,12 @@ Read-Only:
Read-Only:

- `url` (String) The webhook URL

<a id="nestedatt--properties--custom_webhook"></a>
### Nested Schema for `properties.custom_webhook`

Read-Only:

- `url` (String) The custom webhook URL
- `headers` (Map of String) Headers to include in the webhook request
- `body` (String) The body of the webhook request
Loading

0 comments on commit 3277383

Please sign in to comment.