Skip to content

Commit

Permalink
feat: webhook headers (#586)
Browse files Browse the repository at this point in the history
## Short description of the changes

Support headers on the webhook recipient resource.
  • Loading branch information
brookesargent authored Dec 2, 2024
1 parent 0266e56 commit 89f2a89
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 11 deletions.
8 changes: 7 additions & 1 deletion client/recipient.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ type RecipientDetails struct {
WebhookName string `json:"webhook_name,omitempty"`
WebhookURL string `json:"webhook_url,omitempty"`
// webhook only
WebhookSecret string `json:"webhook_secret,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"`
WebhookHeaders []WebhookHeader `json:"webhook_headers"`
// custom webhook
WebhookPayloads *WebhookPayloads `json:"webhook_payloads,omitempty"`
}
Expand Down Expand Up @@ -94,6 +95,11 @@ type TemplateVariable struct {
Default string `json:"default_value"`
}

type WebhookHeader struct {
Key string `json:"header"`
Value string `json:"value"`
}

// RecipientType holds all the possible recipient types.
type RecipientType string

Expand Down
11 changes: 8 additions & 3 deletions client/recipient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ func TestRecipientsCustomWebhook(t *testing.T) {
rcpt: client.Recipient{
Type: client.RecipientTypeWebhook,
Details: client.RecipientDetails{
WebhookName: test.RandomStringWithPrefix("test.", 10),
WebhookURL: test.RandomURL(),
WebhookSecret: "secret",
WebhookName: test.RandomStringWithPrefix("test.", 10),
WebhookURL: test.RandomURL(),
WebhookSecret: "secret",
WebhookHeaders: []client.WebhookHeader{{Key: "Authorization", Value: "Bearer 123"}},
WebhookPayloads: &client.WebhookPayloads{
PayloadTemplates: client.PayloadTemplates{Trigger: &client.PayloadTemplate{Body: body}},
TemplateVariables: []client.TemplateVariable{{Name: "severity", Default: "warning"}},
Expand Down Expand Up @@ -130,6 +131,10 @@ func TestRecipientsCustomWebhook(t *testing.T) {
assert.Equal(t, tr.Details.WebhookSecret, r.Details.WebhookSecret)
assert.Equal(t, tr.Details.WebhookPayloads, r.Details.WebhookPayloads)
assert.Equal(t, tr.Details.WebhookPayloads.TemplateVariables, r.Details.WebhookPayloads.TemplateVariables)
if assert.Len(t, r.Details.WebhookHeaders, 1) {
assert.Equal(t, tr.Details.WebhookHeaders[0].Key, r.Details.WebhookHeaders[0].Key)
assert.Equal(t, tr.Details.WebhookHeaders[0].Value, r.Details.WebhookHeaders[0].Value)
}
})
}
}
Expand Down
18 changes: 16 additions & 2 deletions docs/resources/webhook_recipient.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ resource "honeycombio_webhook_recipient" "prod" {
name = "Production Alerts"
secret = "a63dab148496ecbe04a1a802ca9b95b8"
url = "https://my.url.corp.net"
header {
name = "Authorization"
value = "Bearer 123"
}
template {
type = "trigger"
body = <<EOT
Expand Down Expand Up @@ -50,7 +55,8 @@ The following arguments are supported:
* `secret` - (Optional) The secret to include when sending the notification to the webhook.
* `url` - (Required) The URL of the endpoint to send the notification to.
* `template` - (Optional) Zero or more configuration blocks (described below) to customize the webhook payload if desired.
* `variable` - (Optional) Zero or m ore configuration blocks (described below) to define variables to be used in the webhook payload if desired.
* `variable` - (Optional) Zero or more configuration blocks (described below) to define variables to be used in the webhook payload if desired.
* `header` - (Optional) Zero or more configuration blocks (described below) to add custom webhook headers if desired.

When configuring custom webhook payloads, use the `template` block, which accepts the following arguments:

Expand All @@ -64,6 +70,14 @@ The `variable` block accepts the following arguments:
* `name` - (Required) The name of the custom variable. Must be an alphanumeric string beginning with a lowercase letter.
* `default_value` - (Optional) The default value for the custom variable, which can be overridden at the alert level.

Optionally, when configuring custom webhooks, use the `header` block to create custom HTTP headers to be included in the webhook request.
Up to five custom headers can be configured. Reserved headers `Content-Type`, `User-Agent`, and `X-Honeycomb-Webhook-Token` cannot be used.
The `header` block accepts the following arguments:

* `name` - (Required) The name or key for the header.
* `value` - (Optional) The value for the header.



## Attribute Reference

Expand Down
11 changes: 11 additions & 0 deletions internal/models/recipients.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type WebhookRecipientModel struct {
URL types.String `tfsdk:"url"`
Templates types.Set `tfsdk:"template"` // WebhookTemplateModel
Variables types.Set `tfsdk:"variable"` // TemplateVariableModel
Headers types.Set `tfsdk:"header"` //WebhookHeaderModel
}

type WebhookTemplateModel struct {
Expand All @@ -33,3 +34,13 @@ var TemplateVariableAttrType = map[string]attr.Type{
"name": types.StringType,
"default_value": types.StringType,
}

type WebhookHeaderModel struct {
Name types.String `tfsdk:"name"`
Value types.String `tfsdk:"value"`
}

var WebhookHeaderAttrType = map[string]attr.Type{
"name": types.StringType,
"value": types.StringType,
}
121 changes: 116 additions & 5 deletions internal/provider/webhook_recipient_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"golang.org/x/net/http/httpguts"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
Expand All @@ -33,6 +34,7 @@ var (
_ resource.ResourceWithValidateConfig = &webhookRecipientResource{}

webhookTemplateTypes = []string{"trigger", "exhaustion_time", "budget_rate"}
webhookHeaderDefaults = []string{"Content-Type", "User-Agent", "X-Honeycomb-Webhook-Token"}
webhookTemplateNameRegex = regexp.MustCompile(`^[a-z](?:[a-zA-Z0-9]+$)?$`)
)

Expand Down Expand Up @@ -145,6 +147,33 @@ func (*webhookRecipientResource) Schema(_ context.Context, _ resource.SchemaRequ
},
},
},
"header": schema.SetNestedBlock{
Description: "Custom headers for webhooks",
Validators: []validator.Set{
setvalidator.SizeAtMost(5),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
Description: "The name or key for the header",
Validators: []validator.String{
stringvalidator.LengthBetween(1, 64),
stringvalidator.NoneOfCaseInsensitive(webhookHeaderDefaults...),
},
},
"value": schema.StringAttribute{
Description: "Value for the header",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
Validators: []validator.String{
stringvalidator.LengthAtMost(512),
},
},
},
},
},
},
}
}
Expand All @@ -159,6 +188,7 @@ func (r *webhookRecipientResource) ImportState(ctx context.Context, req resource
ID: types.StringValue(req.ID),
Templates: types.SetUnknown(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}),
Variables: types.SetUnknown(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}),
Headers: types.SetUnknown(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}),
})...)
}

Expand All @@ -177,6 +207,9 @@ func (r *webhookRecipientResource) ValidateConfig(ctx context.Context, req resou
var variables []models.TemplateVariableModel
data.Variables.ElementsAs(ctx, &variables, false)

var headers []models.WebhookHeaderModel
data.Headers.ElementsAs(ctx, &headers, false)

triggerTmplExists := false
budgetRateTmplExists := false
exhaustionTimeTmplExists := false
Expand Down Expand Up @@ -235,6 +268,24 @@ func (r *webhookRecipientResource) ValidateConfig(ctx context.Context, req resou
}
duplicateMap[name] = true
}

// webhook headers must be valid http headers
for i, h := range headers {
if !httpguts.ValidHeaderFieldName(h.Name.ValueString()) {
resp.Diagnostics.AddAttributeError(
path.Root("header").AtListIndex(i).AtName("name"),
"Conflicting configuration arguments",
"invalid webhook header name",
)
}
if !httpguts.ValidHeaderFieldValue(h.Value.ValueString()) {
resp.Diagnostics.AddAttributeError(
path.Root("header").AtListIndex(i).AtName("value"),
"Conflicting configuration arguments",
"invalid webhook header value",
)
}
}
}

func (r *webhookRecipientResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
Expand All @@ -251,6 +302,7 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea
WebhookURL: plan.URL.ValueString(),
WebhookSecret: plan.Secret.ValueString(),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics),
WebhookHeaders: expandWebhookHeaders(ctx, plan.Headers, &resp.Diagnostics),
},
})
if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Webhook Recipient", err) {
Expand All @@ -270,6 +322,7 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea
// to prevent confusing if/else blocks, set null by default and override it if we have that detail on the recipient
state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType})

if rcpt.Details.WebhookPayloads != nil {
state.Templates = plan.Templates
Expand All @@ -278,6 +331,10 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea
}
}

if rcpt.Details.WebhookHeaders != nil {
state.Headers = plan.Headers
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

Expand Down Expand Up @@ -334,6 +391,12 @@ func (r *webhookRecipientResource) Read(ctx context.Context, req resource.ReadRe
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
}

if rcpt.Details.WebhookHeaders != nil {
state.Headers = flattenWebhookHeaders(ctx, rcpt.Details.WebhookHeaders, &resp.Diagnostics)
} else {
state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType})
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

Expand All @@ -351,6 +414,7 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda
WebhookName: plan.Name.ValueString(),
WebhookURL: plan.URL.ValueString(),
WebhookSecret: plan.Secret.ValueString(),
WebhookHeaders: expandWebhookHeaders(ctx, plan.Headers, &resp.Diagnostics),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics),
},
})
Expand All @@ -372,16 +436,21 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda
} else {
state.Secret = types.StringNull()
}

// to prevent confusing if/else blocks, set null by default and override it if we have that detail on the recipient
state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType})

if rcpt.Details.WebhookPayloads != nil {
state.Templates = plan.Templates
if rcpt.Details.WebhookPayloads.TemplateVariables != nil {
state.Variables = plan.Variables
} else {
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
}
} else {
state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
}

if rcpt.Details.WebhookHeaders != nil {
state.Headers = plan.Headers
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
Expand Down Expand Up @@ -522,3 +591,45 @@ func webhookVariableToObjectValue(v client.TemplateVariable, diags *diag.Diagnos

return varObjVal
}

func expandWebhookHeaders(ctx context.Context, set types.Set, diags *diag.Diagnostics) []client.WebhookHeader {
var headers []models.WebhookHeaderModel
diags.Append(set.ElementsAs(ctx, &headers, false)...)
if diags.HasError() {
return nil
}

clientHeaders := make([]client.WebhookHeader, len(headers))
for i, h := range headers {
hdr := client.WebhookHeader{
Key: h.Name.ValueString(),
Value: h.Value.ValueString(),
}

clientHeaders[i] = hdr
}

return clientHeaders
}

func flattenWebhookHeaders(ctx context.Context, hdrs []client.WebhookHeader, diags *diag.Diagnostics) types.Set {
var hdrValues []attr.Value
for _, h := range hdrs {
hdrValues = append(hdrValues, webhookHeaderToObjectValue(h, diags))
}
hdrResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}, hdrValues)
diags.Append(d...)

return hdrResult
}

func webhookHeaderToObjectValue(h client.WebhookHeader, diags *diag.Diagnostics) basetypes.ObjectValue {
headerObj := map[string]attr.Value{
"name": types.StringValue(h.Key),
"value": types.StringValue(h.Value),
}
headerObjVal, d := types.ObjectValue(models.WebhookHeaderAttrType, headerObj)
diags.Append(d...)

return headerObjVal
}
Loading

0 comments on commit 89f2a89

Please sign in to comment.