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

feat: template variables #583

Merged
merged 17 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
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
8 changes: 7 additions & 1 deletion client/recipient.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ type NotificationRecipientDetails struct {
}

type WebhookPayloads struct {
PayloadTemplates PayloadTemplates `json:"payload_templates"`
PayloadTemplates PayloadTemplates `json:"payload_templates"`
TemplateVariables []TemplateVariable `json:"template_variables"`
}

type PayloadTemplates struct {
Expand All @@ -88,6 +89,11 @@ type PayloadTemplate struct {
Body string `json:"body"`
}

type TemplateVariable struct {
Name string `json:"name"`
Default string `json:"default_value"`
}

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

Expand Down
5 changes: 4 additions & 1 deletion client/recipient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ func TestRecipientsCustomWebhook(t *testing.T) {
WebhookURL: test.RandomURL(),
WebhookSecret: "secret",
WebhookPayloads: &client.WebhookPayloads{
PayloadTemplates: client.PayloadTemplates{Trigger: &client.PayloadTemplate{Body: body}}},
PayloadTemplates: client.PayloadTemplates{Trigger: &client.PayloadTemplate{Body: body}},
TemplateVariables: []client.TemplateVariable{{Name: "severity", Default: "warning"}},
},
},
},
},
Expand All @@ -127,6 +129,7 @@ func TestRecipientsCustomWebhook(t *testing.T) {
assert.Equal(t, tr.Details.WebhookURL, r.Details.WebhookURL)
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)
})
}
}
Expand Down
11 changes: 11 additions & 0 deletions internal/models/recipients.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type WebhookRecipientModel struct {
Secret types.String `tfsdk:"secret"`
URL types.String `tfsdk:"url"`
Templates types.Set `tfsdk:"template"` // WebhookTemplateModel
Variables types.Set `tfsdk:"variable"` // TemplateVariableModel
}

type WebhookTemplateModel struct {
Expand All @@ -22,3 +23,13 @@ var WebhookTemplateAttrType = map[string]attr.Type{
"type": types.StringType,
"body": types.StringType,
}

type TemplateVariableModel struct {
Name types.String `tfsdk:"name"`
DefaultValue types.String `tfsdk:"default_value"`
}

var TemplateVariableAttrType = map[string]attr.Type{
"name": types.StringType,
"default_value": types.StringType,
}
135 changes: 122 additions & 13 deletions internal/provider/webhook_recipient_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package provider
import (
"context"
"errors"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
Expand All @@ -29,7 +31,8 @@ var (
_ resource.ResourceWithImportState = &webhookRecipientResource{}
_ resource.ResourceWithValidateConfig = &webhookRecipientResource{}

webhookTemplateTypes = []string{"trigger", "exhaustion_time", "budget_rate"}
webhookTemplateTypes = []string{"trigger", "exhaustion_time", "budget_rate"}
webhookTemplateNameRegex = regexp.MustCompile(`^[a-z](?:[a-zA-Z0-9]+$)?$`)
)

type webhookRecipientResource struct {
Expand Down Expand Up @@ -114,6 +117,31 @@ func (*webhookRecipientResource) Schema(_ context.Context, _ resource.SchemaRequ
},
},
},
"variable": schema.SetNestedBlock{
Description: "Variables for webhook templates",
Validators: []validator.Set{
setvalidator.SizeAtMost(10),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
Description: "The name of the variable",
Validators: []validator.String{
stringvalidator.LengthBetween(1, 64),
stringvalidator.RegexMatches(webhookTemplateNameRegex, "must be an alphanumeric string beginning with a lowercase letter"),
},
},
"default_value": schema.StringAttribute{
Description: "An optional default value for the variable",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtMost(256),
},
},
},
},
},
},
}
}
Expand All @@ -127,6 +155,7 @@ func (r *webhookRecipientResource) ImportState(ctx context.Context, req resource
resp.Diagnostics.Append(resp.State.Set(ctx, &models.WebhookRecipientModel{
ID: types.StringValue(req.ID),
Templates: types.SetUnknown(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}),
Variables: types.SetUnknown(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}),
})...)
}

Expand All @@ -141,6 +170,8 @@ func (r *webhookRecipientResource) ValidateConfig(ctx context.Context, req resou

// only allow one template of each type (trigger, budget_rate, exhaustion_time)
validateAttributesWhenTemplatesIncluded(ctx, data, resp)
// template variables cannot be configured without a template and variable names cannot be duplicated
validateAttributesWhenVariablesIncluded(ctx, data, resp)
}

func (r *webhookRecipientResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
Expand All @@ -156,7 +187,7 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea
WebhookName: plan.Name.ValueString(),
WebhookURL: plan.URL.ValueString(),
WebhookSecret: plan.Secret.ValueString(),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, &resp.Diagnostics),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics),
},
})
if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Webhook Recipient", err) {
Expand All @@ -172,10 +203,16 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea
} 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
brookesargent marked this conversation as resolved.
Show resolved Hide resolved
state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})

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

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
Expand Down Expand Up @@ -226,10 +263,12 @@ func (r *webhookRecipientResource) Read(ctx context.Context, req resource.ReadRe
} else {
state.Secret = types.StringNull()
}

if rcpt.Details.WebhookPayloads != nil {
state.Templates = clientPayloadsToWebhookTemplates(ctx, rcpt.Details.WebhookPayloads, &resp.Diagnostics)
state.Templates, state.Variables = clientPayloadsToSets(ctx, rcpt.Details.WebhookPayloads, &resp.Diagnostics)
} else {
state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
Expand All @@ -249,7 +288,7 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda
WebhookName: plan.Name.ValueString(),
WebhookURL: plan.URL.ValueString(),
WebhookSecret: plan.Secret.ValueString(),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, &resp.Diagnostics),
WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics),
},
})
if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Webhook Recipient", err) {
Expand All @@ -272,8 +311,14 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda
}
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})
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
Expand Down Expand Up @@ -306,9 +351,15 @@ func (r *webhookRecipientResource) Delete(ctx context.Context, req resource.Dele
}
}

func webhookTemplatesToClientPayloads(ctx context.Context, set types.Set, diags *diag.Diagnostics) *client.WebhookPayloads {
func webhookTemplatesToClientPayloads(ctx context.Context, templateSet types.Set, variableSet types.Set, diags *diag.Diagnostics) *client.WebhookPayloads {
var templates []models.WebhookTemplateModel
diags.Append(set.ElementsAs(ctx, &templates, false)...)
diags.Append(templateSet.ElementsAs(ctx, &templates, false)...)
if diags.HasError() {
return nil
}

var variables []models.TemplateVariableModel
diags.Append(variableSet.ElementsAs(ctx, &variables, false)...)
if diags.HasError() {
return nil
}
Expand All @@ -332,19 +383,37 @@ func webhookTemplatesToClientPayloads(ctx context.Context, set types.Set, diags
}
}

clientVars := make([]client.TemplateVariable, len(variables))
for i, v := range variables {
tmplVar := client.TemplateVariable{
Name: v.Name.ValueString(),
Default: v.DefaultValue.ValueString(),
}

clientVars[i] = tmplVar
}
clientWebhookPayloads.TemplateVariables = clientVars

return clientWebhookPayloads
}

func clientPayloadsToWebhookTemplates(ctx context.Context, p *client.WebhookPayloads, diags *diag.Diagnostics) types.Set {
func clientPayloadsToSets(ctx context.Context, p *client.WebhookPayloads, diags *diag.Diagnostics) (types.Set, types.Set) {
brookesargent marked this conversation as resolved.
Show resolved Hide resolved
if p == nil {
return types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType})
return types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}), types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType})
}

values := webhookTemplatesToObjectValues(p.PayloadTemplates, diags)
result, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}, values)
tmplValues := webhookTemplatesToObjectValues(p.PayloadTemplates, diags)
tmplResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}, tmplValues)
diags.Append(d...)

var tmplVarValues []attr.Value
for _, v := range p.TemplateVariables {
tmplVarValues = append(tmplVarValues, webhookVariableToObjectValue(v, diags))
}
varResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.TemplateVariableAttrType}, tmplVarValues)
diags.Append(d...)

return result
return tmplResult, varResult
}

func webhookTemplatesToObjectValues(templates client.PayloadTemplates, diags *diag.Diagnostics) []basetypes.ObjectValue {
Expand Down Expand Up @@ -380,6 +449,17 @@ func webhookTemplatesToObjectValues(templates client.PayloadTemplates, diags *di
return templateObjs
}

func webhookVariableToObjectValue(v client.TemplateVariable, diags *diag.Diagnostics) basetypes.ObjectValue {
variableObj := map[string]attr.Value{
"name": types.StringValue(v.Name),
"default_value": types.StringValue(v.Default),
}
varObjVal, d := types.ObjectValue(models.TemplateVariableAttrType, variableObj)
diags.Append(d...)

return varObjVal
}

func validateAttributesWhenTemplatesIncluded(ctx context.Context, data models.WebhookRecipientModel, resp *resource.ValidateConfigResponse) {
var templates []models.WebhookTemplateModel
data.Templates.ElementsAs(ctx, &templates, false)
Expand Down Expand Up @@ -420,3 +500,32 @@ func validateAttributesWhenTemplatesIncluded(ctx context.Context, data models.We

}
}

func validateAttributesWhenVariablesIncluded(ctx context.Context, data models.WebhookRecipientModel, resp *resource.ValidateConfigResponse) {
var templates []models.WebhookTemplateModel
brookesargent marked this conversation as resolved.
Show resolved Hide resolved
data.Templates.ElementsAs(ctx, &templates, false)

var variables []models.TemplateVariableModel
data.Variables.ElementsAs(ctx, &variables, false)

if len(variables) >= 1 && len(templates) == 0 {
resp.Diagnostics.AddAttributeError(
path.Root("variable").AtListIndex(0),
"Conflicting configuration arguments",
"cannot configure a \"variable\" without also configuring a \"template\"",
)
}

duplicateMap := make(map[string]bool)
for i, v := range variables {
name := v.Name.ValueString()
if duplicateMap[name] {
resp.Diagnostics.AddAttributeError(
path.Root("variable").AtListIndex(i).AtName("name"),
"Conflicting configuration arguments",
"cannot have more than one \"variable\" with the same \"name\"",
)
}
duplicateMap[name] = true
}
}
Loading
Loading