diff --git a/go.mod b/go.mod index 3586aa367..03b27b480 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/PagerDuty/terraform-provider-pagerduty go 1.20 require ( - github.com/PagerDuty/go-pagerduty v1.8.1-0.20241111225923-0ef8f340dd3c + github.com/PagerDuty/go-pagerduty v1.8.1-0.20250113202017-9831333ebe6b github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hc-install v0.6.2 @@ -16,7 +16,7 @@ require ( github.com/hashicorp/terraform-plugin-mux v0.13.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 github.com/hashicorp/terraform-plugin-testing v1.6.0 - github.com/heimweh/go-pagerduty v0.0.0-20240731213000-b0991665ac52 + github.com/heimweh/go-pagerduty v0.0.0-20250113182705-ce1f94dc30af ) require ( diff --git a/go.sum b/go.sum index e62897a25..8601e9670 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/PagerDuty/go-pagerduty v1.8.1-0.20241111225923-0ef8f340dd3c h1:Bnw38sqsVdQVOiuTealk57GnuRb86zyd5v/XW3BcPl8= -github.com/PagerDuty/go-pagerduty v1.8.1-0.20241111225923-0ef8f340dd3c/go.mod h1:ilimTqwHSBjmvKeYA/yayDBZvzf/CX4Pwa9Qbhekzok= +github.com/PagerDuty/go-pagerduty v1.8.1-0.20250113202017-9831333ebe6b h1:Fz0h+POmFi1FkeLCWwrl7MmhPA1EFgVzi3FKzxCG/Dw= +github.com/PagerDuty/go-pagerduty v1.8.1-0.20250113202017-9831333ebe6b/go.mod h1:ilimTqwHSBjmvKeYA/yayDBZvzf/CX4Pwa9Qbhekzok= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/heimweh/go-pagerduty v0.0.0-20240731213000-b0991665ac52 h1:rVc7EhBQpz8+tqx8529IGB076iTXeVgE4RAl7L+mUJ8= -github.com/heimweh/go-pagerduty v0.0.0-20240731213000-b0991665ac52/go.mod h1:r59w5iyN01Qvi734yA5hZldbSeJJmsJzee/1kQ/MK7s= +github.com/heimweh/go-pagerduty v0.0.0-20250113182705-ce1f94dc30af h1:41UndNNQiIhLMxleHdJNTxmpHzT06twPUNRaqfJaSpU= +github.com/heimweh/go-pagerduty v0.0.0-20250113182705-ce1f94dc30af/go.mod h1:r59w5iyN01Qvi734yA5hZldbSeJJmsJzee/1kQ/MK7s= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/pagerdutyplugin/data_source_pagerduty_incident_type.go b/pagerdutyplugin/data_source_pagerduty_incident_type.go new file mode 100644 index 000000000..9fb2be006 --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_incident_type.go @@ -0,0 +1,108 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type dataSourceIncidentType struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceIncidentType)(nil) + +func (*dataSourceIncidentType) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_incident_type" +} + +func (*dataSourceIncidentType) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "name": schema.StringAttribute{Computed: true}, + "type": schema.StringAttribute{Computed: true}, + "display_name": schema.StringAttribute{Required: true}, + "description": schema.StringAttribute{Computed: true}, + "parent_type": schema.StringAttribute{Computed: true}, + "enabled": schema.BoolAttribute{Computed: true}, + }, + } +} + +func (d *dataSourceIncidentType) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceIncidentType) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty incident type") + + var searchName types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("display_name"), &searchName)...) + if resp.Diagnostics.HasError() { + return + } + + var found *pagerduty.IncidentType + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := d.client.ListIncidentTypes(ctx, pagerduty.ListIncidentTypesOptions{Filter: "all"}) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + for _, it := range response.IncidentTypes { + if it.DisplayName == searchName.ValueString() { + found = &it + break + } + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type %s", searchName), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any incident type with the name: %s", searchName), + "", + ) + return + } + + model := dataSourceIncidentTypeModel{ + ID: types.StringValue(found.ID), + Name: types.StringValue(found.Name), + Type: types.StringValue(found.Type), + DisplayName: types.StringValue(found.DisplayName), + Description: types.StringValue(found.Description), + Enabled: types.BoolValue(found.Enabled), + } + if found.Parent != nil { + model.ParentType = types.StringValue(found.Parent.ID) + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceIncidentTypeModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + ParentType types.String `tfsdk:"parent_type"` + Enabled types.Bool `tfsdk:"enabled"` +} diff --git a/pagerdutyplugin/data_source_pagerduty_incident_type_custom_field.go b/pagerdutyplugin/data_source_pagerduty_incident_type_custom_field.go new file mode 100644 index 000000000..ae7c928db --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_incident_type_custom_field.go @@ -0,0 +1,177 @@ +package pagerduty + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type dataSourceIncidentTypeCustomField struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceIncidentTypeCustomField)(nil) + +func (*dataSourceIncidentTypeCustomField) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_incident_type_custom_field" +} + +func (*dataSourceIncidentTypeCustomField) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "incident_type": schema.StringAttribute{Required: true}, + "display_name": schema.StringAttribute{Required: true}, + "data_type": schema.StringAttribute{Computed: true}, + "default_value": schema.StringAttribute{ + Computed: true, + CustomType: jsontypes.NormalizedType{}, + }, + "description": schema.StringAttribute{Computed: true}, + "enabled": schema.BoolAttribute{Computed: true}, + "field_options": schema.ListAttribute{ + Computed: true, + ElementType: incidentTypeFieldOptionObjectType, + }, + "field_type": schema.StringAttribute{Computed: true}, + "name": schema.StringAttribute{Computed: true}, + "self": schema.StringAttribute{Computed: true}, + "summary": schema.StringAttribute{Computed: true}, + "type": schema.StringAttribute{Computed: true}, + }, + } +} + +func (d *dataSourceIncidentTypeCustomField) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceIncidentTypeCustomField) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var searchName types.String + diags := req.Config.GetAttribute(ctx, path.Root("display_name"), &searchName) + if resp.Diagnostics.Append(diags...); diags.HasError() { + return + } + + var searchIncidentType types.String + diags = req.Config.GetAttribute(ctx, path.Root("incident_type"), &searchIncidentType) + if resp.Diagnostics.Append(diags...); diags.HasError() { + return + } + incidentTypeID := searchIncidentType.ValueString() + + log.Printf("[INFO] Reading PagerDuty incident type custom field %s %s", searchIncidentType, searchName) + + var found *pagerduty.IncidentTypeField + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := d.client.ListIncidentTypeFields(ctx, incidentTypeID, pagerduty.ListIncidentTypeFieldsOptions{ + Includes: []string{"field_options"}, + }) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + for _, f := range response.Fields { + if f.DisplayName == searchName.ValueString() { + found = &f + break + } + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type custom field %s", searchName), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any incident type custom field with the name: %s", searchName), + "", + ) + return + } + + defaultValue, _ := json.Marshal(found.DefaultValue) + + elements := make([]attr.Value, 0, len(found.FieldOptions)) + for _, opt := range found.FieldOptions { + dataObj := types.ObjectNull(incidentTypeFieldOptionDataObjectType.AttrTypes) + if opt.Data != nil { + dataObj = types.ObjectValueMust(incidentTypeFieldOptionDataObjectType.AttrTypes, map[string]attr.Value{ + "value": types.StringValue(opt.Data.Value), + "data_type": types.StringValue(opt.Data.DataType), + }) + } + obj := types.ObjectValueMust(incidentTypeFieldOptionObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(opt.ID), + "type": types.StringValue(opt.Type), + "data": dataObj, + }) + elements = append(elements, obj) + } + fieldOptions := types.ListValueMust(incidentTypeFieldOptionObjectType, elements) + + model := dataSourceIncidentTypeCustomFieldModel{ + ID: types.StringValue(found.ID), + IncidentType: types.StringValue(found.IncidentType), + DisplayName: types.StringValue(found.DisplayName), + DataType: types.StringValue(found.DataType), + DefaultValue: jsontypes.NewNormalizedValue(string(defaultValue)), + Description: types.StringValue(found.Description), + Enabled: types.BoolValue(found.Enabled), + FieldOptions: fieldOptions, + FieldType: types.StringValue(found.FieldType), + Name: types.StringValue(found.Name), + Self: types.StringValue(found.Self), + Summary: types.StringValue(found.Summary), + Type: types.StringValue(found.Type), + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceIncidentTypeCustomFieldModel struct { + ID types.String `tfsdk:"id"` + IncidentType types.String `tfsdk:"incident_type"` + DisplayName types.String `tfsdk:"display_name"` + DataType types.String `tfsdk:"data_type"` + DefaultValue jsontypes.Normalized `tfsdk:"default_value"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + FieldOptions types.List `tfsdk:"field_options"` + FieldType types.String `tfsdk:"field_type"` + Name types.String `tfsdk:"name"` + Self types.String `tfsdk:"self"` + Summary types.String `tfsdk:"summary"` + Type types.String `tfsdk:"type"` +} + +var incidentTypeFieldOptionDataObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "value": types.StringType, + "data_type": types.StringType, + }, +} + +var incidentTypeFieldOptionObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + "data": incidentTypeFieldOptionDataObjectType, + }, +} diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 7eac9937d..7fbb19b42 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -56,6 +56,8 @@ func (p *Provider) DataSources(_ context.Context) [](func() datasource.DataSourc func() datasource.DataSource { return &dataSourceAlertGroupingSetting{} }, func() datasource.DataSource { return &dataSourceBusinessService{} }, func() datasource.DataSource { return &dataSourceExtensionSchema{} }, + func() datasource.DataSource { return &dataSourceIncidentTypeCustomField{} }, + func() datasource.DataSource { return &dataSourceIncidentType{} }, func() datasource.DataSource { return &dataSourceIntegration{} }, func() datasource.DataSource { return &dataSourceJiraCloudAccountMapping{} }, func() datasource.DataSource { return &dataSourceLicenses{} }, @@ -76,6 +78,8 @@ func (p *Provider) Resources(_ context.Context) [](func() resource.Resource) { func() resource.Resource { return &resourceBusinessService{} }, func() resource.Resource { return &resourceExtensionServiceNow{} }, func() resource.Resource { return &resourceExtension{} }, + func() resource.Resource { return &resourceIncidentTypeCustomField{} }, + func() resource.Resource { return &resourceIncidentType{} }, func() resource.Resource { return &resourceJiraCloudAccountMappingRule{} }, func() resource.Resource { return &resourceServiceDependency{} }, func() resource.Resource { return &resourceTagAssignment{} }, diff --git a/pagerdutyplugin/resource_pagerduty_incident_type.go b/pagerdutyplugin/resource_pagerduty_incident_type.go new file mode 100644 index 000000000..525dc4b38 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_incident_type.go @@ -0,0 +1,296 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/PagerDuty/terraform-provider-pagerduty/util/validate" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceIncidentType struct{ client *pagerduty.Client } + +var ( + _ resource.ResourceWithConfigure = (*resourceIncidentType)(nil) + _ resource.ResourceWithImportState = (*resourceIncidentType)(nil) +) + +func (r *resourceIncidentType) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_incident_type" +} + +func (r *resourceIncidentType) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validate.StringHasNoSuffix("_default"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "display_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validate.StringHasNoPrefix("PD", "PagerDuty", "Default"), + }, + }, + "parent_type": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{Optional: true}, + "enabled": schema.BoolAttribute{Optional: true, Computed: true}, + "type": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *resourceIncidentType) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceIncidentTypeModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + plan := pagerduty.CreateIncidentTypeOptions{ + Name: model.Name.ValueString(), + DisplayName: model.DisplayName.ValueString(), + ParentType: model.ParentType.ValueString(), + Description: model.Description.ValueStringPointer(), + } + if !model.Enabled.IsNull() && !model.Enabled.IsUnknown() { + plan.Enabled = model.Enabled.ValueBoolPointer() + } + log.Printf("[INFO] Creating PagerDuty incident type %s", plan.Name) + + if list, err := r.client.ListIncidentTypes(ctx, pagerduty.ListIncidentTypesOptions{ + Filter: "disabled", + }); err == nil { + for _, it := range list.IncidentTypes { + if it.Name == plan.Name { + resp.Diagnostics.AddWarning( + "Incident Type disabled", + fmt.Sprintf("Incident Type with name %q already exists but it is disabled", plan.Name), + ) + } + } + } + + var id string + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := r.client.CreateIncidentType(ctx, plan) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + id = response.ID + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error creating PagerDuty incident type %s", plan.Name), + err.Error(), + ) + return + } + + model, err = requestGetIncidentType(ctx, r.client, id, plan.ParentType, true, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type %s", id), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceIncidentType) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id, parent types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Reading PagerDuty incident type %s", id) + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("parent_type"), &parent)...) + if resp.Diagnostics.HasError() { + return + } + + state, err := requestGetIncidentType(ctx, r.client, id.ValueString(), parent.ValueString(), false, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type %s", id), + err.Error(), + ) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceIncidentType) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model resourceIncidentTypeModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + var parent types.String + + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("parent_type"), &parent)...) + if resp.Diagnostics.HasError() { + return + } + + plan := pagerduty.UpdateIncidentTypeOptions{ + DisplayName: model.DisplayName.ValueStringPointer(), + Enabled: model.Enabled.ValueBoolPointer(), + Description: model.Description.ValueStringPointer(), + } + + id := model.ID.ValueString() + log.Printf("[INFO] Updating PagerDuty incident type %s", id) + + if parent.ValueString() != model.ParentType.ValueString() { + resp.Diagnostics.AddWarning( + "Can not update value of field \"parent_type\"", + "", + ) + + } + + incidentType, err := r.client.UpdateIncidentType(ctx, id, plan) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty incident type %s", id), + err.Error(), + ) + return + } + + model, err = flattenIncidentType(ctx, r.client, incidentType, model.ParentType.ValueString(), &resp.Diagnostics) + if err != nil { + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceIncidentType) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddWarning( + "Cannot delete incident type", + "This action has no effect, and you might want to disable your incident type by changing it with `enabled = false`. If you want terraform to stop tracking this resource please use `terraform state rm`.", + ) +} + +func (r *resourceIncidentType) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceIncidentType) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +type resourceIncidentTypeModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + ParentType types.String `tfsdk:"parent_type"` + Enabled types.Bool `tfsdk:"enabled"` + Type types.String `tfsdk:"type"` +} + +func requestGetIncidentType(ctx context.Context, client *pagerduty.Client, id, parent string, retryNotFound bool, diags *diag.Diagnostics) (resourceIncidentTypeModel, error) { + var model resourceIncidentTypeModel + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + incidentType, err := client.GetIncidentType(ctx, id, pagerduty.GetIncidentTypeOptions{}) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if !retryNotFound && util.IsNotFoundError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + + model, err = flattenIncidentType(ctx, client, incidentType, parent, diags) + if err != nil { + return retry.NonRetryableError(err) + } + + return nil + }) + + return model, err +} + +func flattenIncidentType(ctx context.Context, client *pagerduty.Client, response *pagerduty.IncidentType, parent string, diags *diag.Diagnostics) (resourceIncidentTypeModel, error) { + if parent != response.Parent.ID { + incidentType, err := client.GetIncidentType(ctx, parent, pagerduty.GetIncidentTypeOptions{}) + if err != nil { + return resourceIncidentTypeModel{}, err + } + if parent != incidentType.ID && parent != incidentType.Name { + return resourceIncidentTypeModel{}, fmt.Errorf("parent_type %q was not received, got %q (ID=%s)", parent, incidentType.Name, incidentType.ID) + } + } + + model := resourceIncidentTypeModel{ + ID: types.StringValue(response.ID), + Name: types.StringValue(response.Name), + DisplayName: types.StringValue(response.DisplayName), + ParentType: types.StringValue(parent), + Enabled: types.BoolValue(response.Enabled), + Type: types.StringValue(response.Type), + } + + if response.Description != "" { + model.Description = types.StringValue(response.Description) + } + + return model, nil +} diff --git a/pagerdutyplugin/resource_pagerduty_incident_type_custom_field.go b/pagerdutyplugin/resource_pagerduty_incident_type_custom_field.go new file mode 100644 index 000000000..de4c3c314 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_incident_type_custom_field.go @@ -0,0 +1,502 @@ +package pagerduty + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceIncidentTypeCustomField struct{ client *pagerduty.Client } + +var ( + _ resource.ResourceWithConfigure = (*resourceIncidentTypeCustomField)(nil) + _ resource.ResourceWithImportState = (*resourceIncidentTypeCustomField)(nil) +) + +func (r *resourceIncidentTypeCustomField) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_incident_type_custom_field" +} + +func (r *resourceIncidentTypeCustomField) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var model resourceIncidentTypeCustomFieldModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + if model.FieldType.ValueString() == "single_value_fixed" || model.FieldType.ValueString() == "multi_value_fixed" { + if len(model.FieldOptions.Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("field_options"), + "Invalid Value", + "`field_options` can't be empty when `field_type` is `single_value_fixed` or `multi_value_fixed`", + ) + } + } else if model.FieldType.ValueString() == "single_value" || model.FieldType.ValueString() == "multi_value" { + if !model.FieldOptions.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("field_options"), + "Invalid Value", + "field_options: not allowed for field type "+model.FieldType.ValueString(), + ) + } + } + + if model.FieldType.ValueString() != "single_value" { + // The API error response indicates that data_type != string can be single_value or + // single_value_fixed but whenever trying, it fails + if model.DataType.ValueString() != "string" { + resp.Diagnostics.AddAttributeError( + path.Root("field_type"), + "Invalid Value", + "field_type must be single_value when data_type is "+model.DataType.ValueString(), + ) + } + } +} + +func (r *resourceIncidentTypeCustomField) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "incident_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "display_name": schema.StringAttribute{ + Required: true, + }, + "data_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "field_type": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{ + stringvalidator.OneOf("single_value", "multi_value", "single_value_fixed", "multi_value_fixed"), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + }, + "default_value": schema.StringAttribute{ + Optional: true, + CustomType: jsontypes.NormalizedType{}, + }, + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "field_options": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + }, + "summary": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "type": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "self": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + }, + } +} + +func (r *resourceIncidentTypeCustomField) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceIncidentTypeCustomFieldModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Creating PagerDuty incident type custom field %s", model.Name) + + var fieldOptions []pagerduty.IncidentTypeFieldOption + { + var target []types.String + d := model.FieldOptions.ElementsAs(ctx, &target, true) + if resp.Diagnostics.Append(d...); d.HasError() { + return + } + for _, t := range target { + opt := pagerduty.IncidentTypeFieldOption{ + Data: &pagerduty.IncidentTypeFieldOptionData{ + Value: t.ValueString(), + DataType: model.DataType.ValueString(), + }, + Type: "field_option", + } + fieldOptions = append(fieldOptions, opt) + } + if resp.Diagnostics.HasError() { + return + } + } + + var defaultValue any + if !model.DefaultValue.IsNull() && !model.DefaultValue.IsUnknown() { + v := model.DefaultValue.ValueString() + if err := json.Unmarshal([]byte(v), &defaultValue); err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("default_value"), + "Error parsing", + err.Error(), + ) + return + } + } + + enabled := model.Enabled.ValueBoolPointer() + if model.Enabled.IsUnknown() { + enabled = nil + } + + plan := pagerduty.CreateIncidentTypeFieldOptions{ + Name: model.Name.ValueString(), + DisplayName: model.DisplayName.ValueString(), + DataType: model.DataType.ValueString(), + FieldType: model.FieldType.ValueString(), + DefaultValue: defaultValue, + Description: model.Description.ValueStringPointer(), + Enabled: enabled, + FieldOptions: fieldOptions, + } + + incidentTypeID := model.IncidentType.ValueString() + var fieldID string + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := r.client.CreateIncidentTypeField(ctx, model.IncidentType.ValueString(), plan) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + fieldID = response.ID + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error creating PagerDuty incident type custom field %s", plan.Name), + err.Error(), + ) + return + } + + model, err = requestGetIncidentTypeCustomField(ctx, r.client, incidentTypeID, fieldID, false, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type custom field %s for incident type %s", fieldID, incidentTypeID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceIncidentTypeCustomField) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Reading PagerDuty incident type custom field %s", id) + + incidentTypeID, fieldID, err := util.ResourcePagerDutyParseColonCompoundID(id.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError(path.Root("id"), "Invalid Value", err.Error()) + return + } + + state, err := requestGetIncidentTypeCustomField(ctx, r.client, incidentTypeID, fieldID, false, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type custom field %s", id), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceIncidentTypeCustomField) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model resourceIncidentTypeCustomFieldModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Updating PagerDuty incident type custom field %s", model.ID) + + var defaultValue interface{} + if !model.DefaultValue.IsNull() && !model.DefaultValue.IsUnknown() { + v := model.DefaultValue.ValueString() + json.Unmarshal([]byte(v), &defaultValue) + } + + plan := pagerduty.UpdateIncidentTypeFieldOptions{ + DisplayName: model.DisplayName.ValueStringPointer(), + DefaultValue: &defaultValue, + Description: model.Description.ValueStringPointer(), + Enabled: model.Enabled.ValueBoolPointer(), + } + + incidentTypeID, fieldID, err := util.ResourcePagerDutyParseColonCompoundID(model.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError(path.Root("id"), "Invalid Value", err.Error()) + return + } + + var planFieldOptions, stateFieldOptions []string + { + var set types.Set + d := req.State.GetAttribute(ctx, path.Root("field_options"), &set) + if resp.Diagnostics.Append(d...); d.HasError() { + return + } + d = set.ElementsAs(ctx, &stateFieldOptions, true) + if resp.Diagnostics.Append(d...); d.HasError() { + return + } + } + { + d := model.FieldOptions.ElementsAs(ctx, &planFieldOptions, true) + if resp.Diagnostics.Append(d...); d.HasError() { + return + } + } + additions, deletions := util.CalculateDiff(stateFieldOptions, planFieldOptions) + + field, err := r.client.GetIncidentTypeField(ctx, incidentTypeID, fieldID, pagerduty.GetIncidentTypeFieldOptions{ + Includes: []string{"field_options"}, + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty incident type custom field %s", model.ID), + err.Error(), + ) + } + + db := make(map[string]string) + for _, opt := range field.FieldOptions { + if opt.Data == nil { + continue + } + db[opt.Data.Value] = opt.ID + } + + if _, err := r.client.UpdateIncidentTypeField(ctx, incidentTypeID, fieldID, plan); err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty incident type custom field %s", model.ID), + err.Error(), + ) + return + } + + for _, opt := range additions { + _, err := r.client.CreateIncidentTypeFieldOption(ctx, incidentTypeID, fieldID, pagerduty.CreateIncidentTypeFieldOptionPayload{ + Data: &pagerduty.IncidentTypeFieldOptionData{ + Value: opt, + DataType: field.DataType, + }, + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty incident type custom field %s", model.ID), + err.Error(), + ) + } + } + + for _, opt := range deletions { + err := r.client.DeleteIncidentTypeFieldOption(ctx, incidentTypeID, field.ID, db[opt]) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty incident type custom field %s", model.ID), + err.Error(), + ) + } + } + + model, err = requestGetIncidentTypeCustomField(ctx, r.client, incidentTypeID, fieldID, false, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty incident type custom field %s", model.ID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceIncidentTypeCustomField) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var id types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Deleting PagerDuty incident type custom field %s", id) + + incidentTypeID, fieldID, err := util.ResourcePagerDutyParseColonCompoundID(id.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError(path.Root("id"), "Invalid Value", err.Error()) + return + } + + err = r.client.DeleteIncidentTypeField(ctx, incidentTypeID, fieldID) + if err != nil && !util.IsNotFoundError(err) { + resp.Diagnostics.AddError( + fmt.Sprintf("Error deleting PagerDuty incident type custom field %s", id), + err.Error(), + ) + return + } + resp.State.RemoveResource(ctx) +} + +func (r *resourceIncidentTypeCustomField) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceIncidentTypeCustomField) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +type resourceIncidentTypeCustomFieldModel struct { + ID types.String `tfsdk:"id"` + Enabled types.Bool `tfsdk:"enabled"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + Self types.String `tfsdk:"self"` + Description types.String `tfsdk:"description"` + FieldType types.String `tfsdk:"field_type"` + DataType types.String `tfsdk:"data_type"` + DisplayName types.String `tfsdk:"display_name"` + DefaultValue jsontypes.Normalized `tfsdk:"default_value"` + IncidentType types.String `tfsdk:"incident_type"` + Summary types.String `tfsdk:"summary"` + FieldOptions types.Set `tfsdk:"field_options"` +} + +func requestGetIncidentTypeCustomField(ctx context.Context, client *pagerduty.Client, incidentTypeID, fieldID string, retryNotFound bool, diags *diag.Diagnostics) (resourceIncidentTypeCustomFieldModel, error) { + var model resourceIncidentTypeCustomFieldModel + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + field, err := client.GetIncidentTypeField(ctx, incidentTypeID, fieldID, pagerduty.GetIncidentTypeFieldOptions{ + Includes: []string{"field_options"}, + }) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if !retryNotFound && util.IsNotFoundError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + model = flattenIncidentTypeCustomField(field) + return nil + }) + + return model, err +} + +func flattenIncidentTypeCustomField(response *pagerduty.IncidentTypeField) resourceIncidentTypeCustomFieldModel { + fieldOptions := types.SetNull(types.StringType) + if len(response.FieldOptions) > 0 { + list := make([]attr.Value, 0, len(response.FieldOptions)) + for _, opt := range response.FieldOptions { + if opt.Data != nil { + v := types.StringValue(opt.Data.Value) + list = append(list, v) + } + } + fieldOptions = types.SetValueMust(types.StringType, list) + } + + model := resourceIncidentTypeCustomFieldModel{ + ID: types.StringValue(response.IncidentType + ":" + response.ID), + Enabled: types.BoolValue(response.Enabled), + Self: types.StringNull(), + Name: types.StringValue(response.Name), + Type: types.StringValue(response.Type), + FieldType: types.StringValue(response.FieldType), + DataType: types.StringValue(response.DataType), + DisplayName: types.StringValue(response.DisplayName), + DefaultValue: jsontypes.NewNormalizedNull(), + IncidentType: types.StringValue(response.IncidentType), + Summary: types.StringValue(response.Summary), + FieldOptions: fieldOptions, + } + + if response.Description != "" { + model.Description = types.StringValue(response.Description) + } + + if response.DefaultValue != nil { + buf, err := json.Marshal(response.DefaultValue) + if err == nil { + model.DefaultValue = jsontypes.NewNormalizedValue(string(buf)) + } + } + + if response.Self != "" { + model.Self = types.StringValue(response.Self) + } + + return model +} diff --git a/pagerdutyplugin/resource_pagerduty_incident_type_custom_field_test.go b/pagerdutyplugin/resource_pagerduty_incident_type_custom_field_test.go new file mode 100644 index 000000000..3f3927ff6 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_incident_type_custom_field_test.go @@ -0,0 +1,199 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "strings" + "testing" + + "github.com/PagerDuty/go-pagerduty" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func init() { + resource.AddTestSweepers("pagerduty_incident_type_custom_field", &resource.Sweeper{ + Name: "pagerduty_incident_type_custom_field", + F: func(_ string) error { + ctx := context.Background() + + resp1, err := testAccProvider.client.ListIncidentTypes(ctx, pagerduty.ListIncidentTypesOptions{}) + if err != nil { + return err + } + + sweepForIncidentType := func(incidentType string) error { + resp, err := testAccProvider.client.ListIncidentTypeFields(ctx, incidentType, pagerduty.ListIncidentTypeFieldsOptions{}) + if err != nil { + return err + } + + for _, f := range resp.Fields { + if strings.HasPrefix(f.Name, "test") || strings.HasPrefix(f.Name, "tf_") { + log.Printf("Destroying add-on %s (%s)", f.Name, f.ID) + if err := testAccProvider.client.DeleteIncidentTypeField(ctx, incidentType, f.ID); err != nil { + return err + } + } + } + + return nil + } + + for _, it := range resp1.IncidentTypes { + if strings.HasPrefix(it.Name, "test") || strings.HasPrefix(it.Name, "tf_") { + if err := sweepForIncidentType(it.ID); err != nil { + return err + } + } + } + + return nil + }, + }) +} + +func TestAccPagerDutyIncidentTypeCustomField_Basic(t *testing.T) { + name := fmt.Sprintf("tf_%s", acctest.RandString(5)) + nameUpdated := fmt.Sprintf("tf_%s", acctest.RandString(5)) + enabledUpdated := "false" + descriptionUpdated := fmt.Sprintf("tf_%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyIncidentTypeCustomFieldDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyIncidentTypeCustomFieldConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyIncidentTypeCustomFieldExists("pagerduty_incident_type_custom_field.foo"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "name", name), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "display_name", name), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "enabled", "true"), + resource.TestCheckResourceAttrSet( + "pagerduty_incident_type_custom_field.foo", "incident_type"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "data_type", "string"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_type", "single_value_fixed"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_options.0", "hello"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_options.1", "hi"), + ), + }, + { + Config: testAccCheckPagerDutyIncidentTypeCustomFieldConfigUpdated(name, nameUpdated, enabledUpdated, descriptionUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyIncidentTypeCustomFieldExists("pagerduty_incident_type_custom_field.foo"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "name", name), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "display_name", nameUpdated), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "enabled", enabledUpdated), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "description", descriptionUpdated), + resource.TestCheckResourceAttrSet( + "pagerduty_incident_type_custom_field.foo", "incident_type"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "data_type", "string"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_type", "single_value_fixed"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_options.0", "hello"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "field_options.1", "hi"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type_custom_field.foo", "default_value", "\"hi\""), + ), + }, + }, + }) +} + +func testAccCheckPagerDutyIncidentTypeCustomFieldDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "pagerduty_incident_type_custom_field" { + continue + } + + ctx := context.Background() + parts := strings.Split(rs.Primary.ID, ":") + incidentType, id := parts[0], parts[1] + if _, err := testAccProvider.client.GetIncidentTypeField(ctx, incidentType, id, pagerduty.GetIncidentTypeFieldOptions{}); err == nil { + return fmt.Errorf("Incident type custom field still exists") + } + + } + return nil +} + +func testAccCheckPagerDutyIncidentTypeCustomFieldExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ctx := context.Background() + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + parts := strings.Split(rs.Primary.ID, ":") + incidentType, id := parts[0], parts[1] + _, err := testAccProvider.client.GetIncidentTypeField(ctx, incidentType, id, pagerduty.GetIncidentTypeFieldOptions{}) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckPagerDutyIncidentTypeCustomFieldConfig(name string) string { + incidentType := fmt.Sprintf("tf_%s", acctest.RandString(5)) + return fmt.Sprintf(` +resource "pagerduty_incident_type" "a" { + name = "%s" + display_name = "%[1]s" + parent_type = "incident_default" +} +resource "pagerduty_incident_type_custom_field" "foo" { + name = "%s" + display_name = "%[2]s" + data_type = "string" + field_options = ["hello", "hi"] + field_type = "single_value_fixed" + incident_type = pagerduty_incident_type.a.id +}`, incidentType, name) +} + +func testAccCheckPagerDutyIncidentTypeCustomFieldConfigUpdated(name, nameUpdated, enabledUpdated, descriptionUpdated string) string { + incidentType := fmt.Sprintf("tf_%s", acctest.RandString(5)) + return fmt.Sprintf(` +resource "pagerduty_incident_type" "a" { + name = "%s" + display_name = "%[1]s" + parent_type = "incident_default" +} +resource "pagerduty_incident_type_custom_field" "foo" { + name = "%s" + display_name = "%s" + data_type = "string" + field_options = ["hello", "hi"] + field_type = "single_value_fixed" + incident_type = pagerduty_incident_type.a.id + enabled = %s + description = "%s" + default_value = jsonencode("hi") +}`, incidentType, name, nameUpdated, enabledUpdated, descriptionUpdated) +} diff --git a/pagerdutyplugin/resource_pagerduty_incident_type_test.go b/pagerdutyplugin/resource_pagerduty_incident_type_test.go new file mode 100644 index 000000000..c0d047336 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_incident_type_test.go @@ -0,0 +1,101 @@ +package pagerduty + +import ( + "context" + "fmt" + "testing" + + "github.com/PagerDuty/go-pagerduty" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccPagerDutyIncidentType_Basic(t *testing.T) { + name := fmt.Sprintf("tf_%s", acctest.RandString(5)) + displayName := fmt.Sprintf("Terraform Test Incident Type %s", acctest.RandString(5)) + parentType := "incident_default" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyIncidentTypeConfig(name, displayName, parentType), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyIncidentTypeExists("pagerduty_incident_type.test"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "name", name), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "display_name", displayName), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "parent_type", parentType), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "enabled", "true"), + ), + }, + { + Config: testAccCheckPagerDutyIncidentTypeConfigUpdated(name, displayName+"_updated", parentType), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyIncidentTypeExists("pagerduty_incident_type.test"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "name", name), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "display_name", displayName+"_updated"), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "parent_type", parentType), + resource.TestCheckResourceAttr( + "pagerduty_incident_type.test", "enabled", "false"), + ), + }, + }, + }) +} + +func testAccCheckPagerDutyIncidentTypeExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Incident Type ID is set") + } + + client := testAccProvider.client + ctx := context.Background() + found, err := client.GetIncidentType(ctx, rs.Primary.ID, pagerduty.GetIncidentTypeOptions{}) + if err != nil { + return err + } + if found.ID != rs.Primary.ID { + return fmt.Errorf("Incident Type not found: %v - %v", rs.Primary.ID, found) + } + + return nil + } +} + +func testAccCheckPagerDutyIncidentTypeConfig(name, displayName, parentType string) string { + return fmt.Sprintf(` +resource "pagerduty_incident_type" "test" { + name = "%s" + display_name = "%s" + parent_type = "%s" + description = "Terraform test incident type" + enabled = true +} +`, name, displayName, parentType) +} + +func testAccCheckPagerDutyIncidentTypeConfigUpdated(name, displayName, parentType string) string { + return fmt.Sprintf(` +resource "pagerduty_incident_type" "test" { + name = "%s" + display_name = "%s" + parent_type = "%s" + description = "Terraform test incident type updated" + enabled = false +} +`, name, displayName, parentType) +} diff --git a/util/util.go b/util/util.go index 3e3677d68..fe472f4d3 100644 --- a/util/util.go +++ b/util/util.go @@ -493,4 +493,32 @@ func CheckJSONEqual(expected string) resource.CheckResourceAttrWithFunc { }) } +// Returns a pair of lists with additions and removals necessary to make set +// `from` turn into set `to`. +func CalculateDiff(from, to []string) (additions, deletions []string) { + setA := make(map[string]struct{}) + for _, a := range from { + setA[a] = struct{}{} + } + + setB := make(map[string]struct{}) + for _, b := range to { + setB[b] = struct{}{} + } + + for b := range setB { + if _, found := setA[b]; !found { + additions = append(additions, b) + } + } + + for a := range setA { + if _, found := setB[a]; !found { + deletions = append(deletions, a) + } + } + + return +} + var UserAgentAppend string diff --git a/util/validate/string_has_no_prefix.go b/util/validate/string_has_no_prefix.go new file mode 100644 index 000000000..0f38c9adc --- /dev/null +++ b/util/validate/string_has_no_prefix.go @@ -0,0 +1,38 @@ +package validate + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +type stringHasNoPrefix struct { + prefixes []string +} + +var _ validator.String = (*stringHasNoPrefix)(nil) + +func (v *stringHasNoPrefix) Description(context.Context) string { + list := strings.Join(v.prefixes, ", ") + return "Validates string does not start with any of these: " + list +} + +func (v *stringHasNoPrefix) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v *stringHasNoPrefix) ValidateString(ctx context.Context, req validator.StringRequest, res *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + for _, prefix := range v.prefixes { + if strings.HasPrefix(req.ConfigValue.ValueString(), prefix) { + res.Diagnostics.AddError("Invalid Value", "string should not have prefix "+prefix) + } + } +} + +func StringHasNoPrefix(prefixes ...string) validator.String { + return &stringHasNoPrefix{prefixes: prefixes} +} diff --git a/util/validate/string_has_no_suffix.go b/util/validate/string_has_no_suffix.go new file mode 100644 index 000000000..8394af112 --- /dev/null +++ b/util/validate/string_has_no_suffix.go @@ -0,0 +1,38 @@ +package validate + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +type stringHasNoSuffix struct { + suffixes []string +} + +var _ validator.String = (*stringHasNoSuffix)(nil) + +func (v *stringHasNoSuffix) Description(context.Context) string { + list := strings.Join(v.suffixes, ", ") + return "Validates string does not end with any of these: " + list +} + +func (v *stringHasNoSuffix) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v *stringHasNoSuffix) ValidateString(ctx context.Context, req validator.StringRequest, res *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + for _, suffix := range v.suffixes { + if strings.HasSuffix(req.ConfigValue.ValueString(), suffix) { + res.Diagnostics.AddError("Invalid Value", "string should not have suffix "+suffix) + } + } +} + +func StringHasNoSuffix(suffixes ...string) validator.String { + return &stringHasNoSuffix{suffixes: suffixes} +} diff --git a/vendor/github.com/PagerDuty/go-pagerduty/incident.go b/vendor/github.com/PagerDuty/go-pagerduty/incident.go index b6f8d85e6..55dfe3446 100644 --- a/vendor/github.com/PagerDuty/go-pagerduty/incident.go +++ b/vendor/github.com/PagerDuty/go-pagerduty/incident.go @@ -225,6 +225,7 @@ type ManageIncidentsOptions struct { Status string `json:"status,omitempty"` Title string `json:"title,omitempty"` Priority *APIReference `json:"priority,omitempty"` + Urgency string `json:"urgency,omitempty"` Assignments []Assignee `json:"assignments,omitempty"` EscalationLevel uint `json:"escalation_level,omitempty"` EscalationPolicy *APIReference `json:"escalation_policy,omitempty"` @@ -540,21 +541,29 @@ func (c *Client) CreateIncidentNote(id string, note IncidentNote) error { return err } +type SnoozeIncidentOptions struct { + From string + Duration uint +} + // SnoozeIncidentWithResponse sets an incident to not alert for a specified // period of time. // // Deprecated: Use SnoozeIncidentWithContext instead. -func (c *Client) SnoozeIncidentWithResponse(id string, duration uint) (*Incident, error) { - return c.SnoozeIncidentWithContext(context.Background(), id, duration) +func (c *Client) SnoozeIncidentWithResponse(id string, o SnoozeIncidentOptions) (*Incident, error) { + return c.SnoozeIncidentWithContext(context.Background(), id, o) } // SnoozeIncidentWithContext sets an incident to not alert for a specified period of time. -func (c *Client) SnoozeIncidentWithContext(ctx context.Context, id string, duration uint) (*Incident, error) { - d := map[string]uint{ - "duration": duration, +func (c *Client) SnoozeIncidentWithContext(ctx context.Context, id string, o SnoozeIncidentOptions) (*Incident, error) { + headers := map[string]string{ + "From": o.From, + } + body := map[string]uint{ + "duration": o.Duration, } - resp, err := c.post(ctx, "/incidents/"+id+"/snooze", d, nil) + resp, err := c.post(ctx, "/incidents/"+id+"/snooze", body, headers) if err != nil { return nil, err } @@ -570,10 +579,13 @@ func (c *Client) SnoozeIncidentWithContext(ctx context.Context, id string, durat // SnoozeIncident sets an incident to not alert for a specified period of time. // // Deprecated: Use SnoozeIncidentWithContext instead. -func (c *Client) SnoozeIncident(id string, duration uint) error { +func (c *Client) SnoozeIncident(id string, o SnoozeIncidentOptions) error { + headers := make(map[string]string) + headers["From"] = o.From data := make(map[string]uint) - data["duration"] = duration - _, err := c.post(context.Background(), "/incidents/"+id+"/snooze", data, nil) + data["duration"] = o.Duration + + _, err := c.post(context.Background(), "/incidents/"+id+"/snooze", data, headers) return err } diff --git a/vendor/github.com/PagerDuty/go-pagerduty/incident_type.go b/vendor/github.com/PagerDuty/go-pagerduty/incident_type.go new file mode 100644 index 000000000..4aca632b7 --- /dev/null +++ b/vendor/github.com/PagerDuty/go-pagerduty/incident_type.go @@ -0,0 +1,358 @@ +package pagerduty + +import ( + "context" + + "github.com/google/go-querystring/query" +) + +// IncidentType is allows to categorize incidents, such as a security incident, a major incident, or a fraud incident. +type IncidentType struct { + Enabled bool `json:"enabled,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Parent *APIReference `json:"parent,omitempty"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + DisplayName string `json:"display_name,omitempty"` +} + +type incidentTypeResponse struct { + IncidentType IncidentType `json:"incident_type"` +} + +// ListIncidentsTypesOptions is the structure used when passing parameters to the ListIncidentTypes API endpoint. +type ListIncidentTypesOptions struct { + Filter string `url:"filter,omitempty"` // enabled disabled all +} + +// ListIncidentsTypesResponse is the response structure when calling the ListIncidentTypes API endpoint. +type ListIncidentTypesResponse struct { + IncidentTypes []IncidentType `json:"incident_types"` +} + +// ListIncidentTypes list the available incident types. +func (c *Client) ListIncidentTypes(ctx context.Context, o ListIncidentTypesOptions) (*ListIncidentTypesResponse, error) { + v, err := query.Values(o) + if err != nil { + return nil, err + } + + resp, err := c.get(ctx, "/incidents/types?"+v.Encode(), nil) + if err != nil { + return nil, err + } + + var result ListIncidentTypesResponse + err = c.decodeJSON(resp, &result) + + return &result, err +} + +// CreateIncidentTypeOptions contains the parameters for creating a new incident type. +type CreateIncidentTypeOptions struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + ParentType string `json:"parent_type"` + Enabled *bool `json:"enabled,omitempty"` + Description *string `json:"description,omitempty"` +} + +// CreateIncidentType creates a new incident type. +func (c *Client) CreateIncidentType(ctx context.Context, o CreateIncidentTypeOptions) (*IncidentType, error) { + d := map[string]CreateIncidentTypeOptions{ + "incident_type": o, + } + + resp, err := c.post(ctx, "/incidents/types", d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeResponse + err = c.decodeJSON(resp, &result) + + return &result.IncidentType, err +} + +// GetIncidentTypeOptions contains the parameters for retrieving a specific incident type. +type GetIncidentTypeOptions struct{} + +// GetIncidentType retrieves a specific incident type by ID or name. +func (c *Client) GetIncidentType(ctx context.Context, idOrName string, o GetIncidentTypeOptions) (*IncidentType, error) { + resp, err := c.get(ctx, "/incidents/types/"+idOrName, nil) + if err != nil { + return nil, err + } + + var result incidentTypeResponse + err = c.decodeJSON(resp, &result) + + return &result.IncidentType, err +} + +// UpdateIncidentTypeOptions contains the parameters for updating an incident type. +type UpdateIncidentTypeOptions struct { + DisplayName *string `json:"display_name,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Description *string `json:"description,omitempty"` +} + +// UpdateIncidentType updates an existing incident type with the provided options. +func (c *Client) UpdateIncidentType(ctx context.Context, idOrName string, o UpdateIncidentTypeOptions) (*IncidentType, error) { + d := map[string]UpdateIncidentTypeOptions{ + "incident_type": o, + } + + resp, err := c.put(ctx, "/incidents/types/"+idOrName, d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeResponse + err = c.decodeJSON(resp, &result) + + return &result.IncidentType, err +} + +// IncidentTypeField represents a custom field configuration for an incident type. +type IncidentTypeField struct { + Enabled bool `json:"enabled,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Self string `json:"self,omitempty"` + Description string `json:"description,omitempty"` + FieldType string `json:"field_type,omitempty"` // single_value single_value_fixed multi_value multi_value_fixed + DataType string `json:"data_type,omitempty"` // boolean integer float string datetime url + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + DisplayName string `json:"display_name,omitempty"` + DefaultValue interface{} `json:"default_value,omitempty"` + IncidentType string `json:"incident_type,omitempty"` + Summary string `json:"summary,omitempty"` + FieldOptions []IncidentTypeFieldOption `json:"field_options,omitempty"` +} + +// IncidentTypeFieldOption represents an option for a custom field. +type IncidentTypeFieldOption struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + Data *IncidentTypeFieldOptionData `json:"data,omitempty"` +} + +// IncidentTypeFieldOptionData represents the data for a field option. +type IncidentTypeFieldOptionData struct { + Value string `json:"value,omitempty"` + DataType string `json:"data_type,omitempty"` +} + +// ListIncidentTypeFieldsOptions contains the parameters for listing incident type fields. +type ListIncidentTypeFieldsOptions struct { + Includes []string `url:"include,omitempty,brackets"` +} + +// ListIncidentTypeFieldsResponse represents the response from listing incident type fields. +type ListIncidentTypeFieldsResponse struct { + Fields []IncidentTypeField `json:"fields,omitempty"` +} + +// ListIncidentTypeFields retrieves all custom fields for a specific incident type. +func (c *Client) ListIncidentTypeFields(ctx context.Context, typeIDOrName string, o ListIncidentTypeFieldsOptions) (*ListIncidentTypeFieldsResponse, error) { + v, err := query.Values(o) + if err != nil { + return nil, err + } + + resp, err := c.get(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields?"+v.Encode(), nil) + if err != nil { + return nil, err + } + + var result ListIncidentTypeFieldsResponse + err = c.decodeJSON(resp, &result) + + return &result, err +} + +type incidentTypeFieldsResponse struct { + Field IncidentTypeField `json:"field"` +} + +// CreateIncidentTypeFieldOptions contains the parameters for creating a new incident type field. +type CreateIncidentTypeFieldOptions struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + DataType string `json:"data_type"` // boolean integer float string datetime url + FieldType string `json:"field_type"` // single_value single_value_fixed multi_value multi_value_fixed + DefaultValue interface{} `json:"default_value,omitempty"` + Description *string `json:"description,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + FieldOptions []IncidentTypeFieldOption `json:"field_options,omitempty"` +} + +// CreateIncidentTypeField creates a new custom field for a specific incident type. +func (c *Client) CreateIncidentTypeField(ctx context.Context, typeIDOrName string, o CreateIncidentTypeFieldOptions) (*IncidentTypeField, error) { + d := map[string]CreateIncidentTypeFieldOptions{ + "field": o, + } + + resp, err := c.post(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields", d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldsResponse + err = c.decodeJSON(resp, &result) + + return &result.Field, err +} + +// GetIncidentTypeFieldOptions contains the parameters for retrieving a specific incident type field. +type GetIncidentTypeFieldOptions struct { + Includes []string `url:"include,omitempty,brackets"` +} + +// GetIncidentTypeField retrieves a specific custom field for an incident type. +func (c *Client) GetIncidentTypeField(ctx context.Context, typeIDOrName string, fieldID string, o GetIncidentTypeFieldOptions) (*IncidentTypeField, error) { + v, err := query.Values(o) + if err != nil { + return nil, err + } + + resp, err := c.get(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"?"+v.Encode(), nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldsResponse + err = c.decodeJSON(resp, &result) + + return &result.Field, err +} + +// UpdateIncidentTypeFieldOptions contains the parameters for updating an incident type field. +type UpdateIncidentTypeFieldOptions struct { + DisplayName *string `json:"display_name,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + DefaultValue *interface{} `json:"default_value,omitempty"` + Description *string `json:"description,omitempty"` +} + +// UpdateIncidentTypeField updates an existing custom field for an incident type. +func (c *Client) UpdateIncidentTypeField(ctx context.Context, typeIDOrName, fieldID string, o UpdateIncidentTypeFieldOptions) (*IncidentTypeField, error) { + d := map[string]UpdateIncidentTypeFieldOptions{ + "field": o, + } + + resp, err := c.put(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID, d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldsResponse + err = c.decodeJSON(resp, &result) + + return &result.Field, err +} + +// DeleteIncidentTypeField removes a custom field from an incident type. +func (c *Client) DeleteIncidentTypeField(ctx context.Context, typeIDOrName, fieldID string) error { + _, err := c.delete(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID) + return err +} + +// ListIncidentTypeFieldOptionsOptions contains the parameters for listing field options. +type ListIncidentTypeFieldOptionsOptions struct{} + +// ListIncidentTypeFieldOptionsResponse represents the response from listing field options. +type ListIncidentTypeFieldOptionsResponse struct { + FieldOptions []IncidentTypeFieldOption `json:"field_options,omitempty"` +} + +// ListIncidentTypeFieldOptions retrieves all options for a specific custom field. +func (c *Client) ListIncidentTypeFieldOptions(ctx context.Context, typeIDOrName, fieldID string, o ListIncidentTypeFieldOptionsOptions) (*ListIncidentTypeFieldOptionsResponse, error) { + resp, err := c.get(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"/field_options", nil) + if err != nil { + return nil, err + } + + var result ListIncidentTypeFieldOptionsResponse + err = c.decodeJSON(resp, &result) + + return &result, err +} + +type incidentTypeFieldOptionsResponse struct { + FieldOption IncidentTypeFieldOption `json:"field_option,omitempty"` +} + +// CreateIncidentTypeFieldOptionPayload contains the parameters for creating a new field option. +type CreateIncidentTypeFieldOptionPayload struct { + Data *IncidentTypeFieldOptionData `json:"data,omitempty"` +} + +// CreateIncidentTypeFieldOption creates a new option for a custom field. +func (c *Client) CreateIncidentTypeFieldOption(ctx context.Context, typeIDOrName, fieldID string, o CreateIncidentTypeFieldOptionPayload) (*IncidentTypeFieldOption, error) { + d := map[string]CreateIncidentTypeFieldOptionPayload{ + "field_option": o, + } + + resp, err := c.post(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"/field_options", d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldOptionsResponse + err = c.decodeJSON(resp, &result) + + return &result.FieldOption, err +} + +// GetIncidentTypeFieldOptionOptions contains the parameters for retrieving a specific field option. +type GetIncidentTypeFieldOptionOptions struct{} + +// GetIncidentTypeFieldOption retrieves a specific option for a custom field. +func (c *Client) GetIncidentTypeFieldOption(ctx context.Context, typeIDOrName, fieldID, fieldOptionID string, o GetIncidentTypeFieldOptionOptions) (*IncidentTypeFieldOption, error) { + resp, err := c.get(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"/field_options/"+fieldOptionID, nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldOptionsResponse + err = c.decodeJSON(resp, &result) + + return &result.FieldOption, err +} + +// UpdateIncidentTypeFieldOptionPayload contains the parameters for updating a field option. +type UpdateIncidentTypeFieldOptionPayload struct { + ID string `json:"id,omitempty"` + Data *IncidentTypeFieldOptionData `json:"data,omitempty"` +} + +// UpdateIncidentTypeFieldOption updates an existing option for a custom field. +func (c *Client) UpdateIncidentTypeFieldOption(ctx context.Context, typeIDOrName, fieldID string, o UpdateIncidentTypeFieldOptionPayload) (*IncidentTypeFieldOption, error) { + d := map[string]UpdateIncidentTypeFieldOptionPayload{ + "field_option": o, + } + + resp, err := c.put(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"/field_options/"+o.ID, d, nil) + if err != nil { + return nil, err + } + + var result incidentTypeFieldOptionsResponse + err = c.decodeJSON(resp, &result) + + return &result.FieldOption, err +} + +// DeleteIncidentTypeFieldOption removes an option from a custom field. +func (c *Client) DeleteIncidentTypeFieldOption(ctx context.Context, typeIDOrName, fieldID, fieldOptionID string) error { + _, err := c.delete(ctx, "/incidents/types/"+typeIDOrName+"/custom_fields/"+fieldID+"/field_options/"+fieldOptionID) + return err +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/doc.go new file mode 100644 index 000000000..115960d26 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package boolplanmodifier provides plan modifiers for types.Bool attributes. +package boolplanmodifier diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace.go new file mode 100644 index 000000000..10e84a33d --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplace returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// +// Use RequiresReplaceIfConfigured if the resource replacement should +// only occur if there is a configuration value (ignore unconfigured drift +// detection changes). Use RequiresReplaceIf if the resource replacement +// should check provider-defined conditional logic. +func RequiresReplace() planmodifier.Bool { + return RequiresReplaceIf( + func(_ context.Context, _ planmodifier.BoolRequest, resp *RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true + }, + "If the value of this attribute changes, Terraform will destroy and recreate the resource.", + "If the value of this attribute changes, Terraform will destroy and recreate the resource.", + ) +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if.go new file mode 100644 index 000000000..389ddb937 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIf returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// - The given function returns true. Returning false will not unset any +// prior resource replacement. +// +// Use RequiresReplace if the resource replacement should always occur on value +// changes. Use RequiresReplaceIfConfigured if the resource replacement should +// occur on value changes, but only if there is a configuration value (ignore +// unconfigured drift detection changes). +func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) planmodifier.Bool { + return requiresReplaceIfModifier{ + ifFunc: f, + description: description, + markdownDescription: markdownDescription, + } +} + +// requiresReplaceIfModifier is an plan modifier that sets RequiresReplace +// on the attribute if a given function is true. +type requiresReplaceIfModifier struct { + ifFunc RequiresReplaceIfFunc + description string + markdownDescription string +} + +// Description returns a human-readable description of the plan modifier. +func (m requiresReplaceIfModifier) Description(_ context.Context) string { + return m.description +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m requiresReplaceIfModifier) MarkdownDescription(_ context.Context) string { + return m.markdownDescription +} + +// PlanModifyBool implements the plan modification logic. +func (m requiresReplaceIfModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + // Do not replace on resource creation. + if req.State.Raw.IsNull() { + return + } + + // Do not replace on resource destroy. + if req.Plan.Raw.IsNull() { + return + } + + // Do not replace if the plan and state values are equal. + if req.PlanValue.Equal(req.StateValue) { + return + } + + ifFuncResp := &RequiresReplaceIfFuncResponse{} + + m.ifFunc(ctx, req, ifFuncResp) + + resp.Diagnostics.Append(ifFuncResp.Diagnostics...) + resp.RequiresReplace = ifFuncResp.RequiresReplace +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_configured.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_configured.go new file mode 100644 index 000000000..ca6c4348d --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_configured.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIfConfigured returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// - The configuration value is not null. +// +// Use RequiresReplace if the resource replacement should occur regardless of +// the presence of a configuration value. Use RequiresReplaceIf if the resource +// replacement should check provider-defined conditional logic. +func RequiresReplaceIfConfigured() planmodifier.Bool { + return RequiresReplaceIf( + func(_ context.Context, req planmodifier.BoolRequest, resp *RequiresReplaceIfFuncResponse) { + if req.ConfigValue.IsNull() { + return + } + + resp.RequiresReplace = true + }, + "If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.", + "If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.", + ) +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_func.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_func.go new file mode 100644 index 000000000..d38abd6c4 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/requires_replace_if_func.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIfFunc is a conditional function used in the RequiresReplaceIf +// plan modifier to determine whether the attribute requires replacement. +type RequiresReplaceIfFunc func(context.Context, planmodifier.BoolRequest, *RequiresReplaceIfFuncResponse) + +// RequiresReplaceIfFuncResponse is the response type for a RequiresReplaceIfFunc. +type RequiresReplaceIfFuncResponse struct { + // Diagnostics report errors or warnings related to this logic. An empty + // or unset slice indicates success, with no warnings or errors generated. + Diagnostics diag.Diagnostics + + // RequiresReplace should be enabled if the resource should be replaced. + RequiresReplace bool +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/use_state_for_unknown.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/use_state_for_unknown.go new file mode 100644 index 000000000..efab19637 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier/use_state_for_unknown.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// UseStateForUnknown returns a plan modifier that copies a known prior state +// value into the planned value. Use this when it is known that an unconfigured +// value will remain the same after a resource update. +// +// To prevent Terraform errors, the framework automatically sets unconfigured +// and Computed attributes to an unknown value "(known after apply)" on update. +// Using this plan modifier will instead display the prior state value in the +// plan, unless a prior plan modifier adjusts the value. +func UseStateForUnknown() planmodifier.Bool { + return useStateForUnknownModifier{} +} + +// useStateForUnknownModifier implements the plan modifier. +type useStateForUnknownModifier struct{} + +// Description returns a human-readable description of the plan modifier. +func (m useStateForUnknownModifier) Description(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// PlanModifyBool implements the plan modification logic. +func (m useStateForUnknownModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + // Do nothing if there is no state value. + if req.StateValue.IsNull() { + return + } + + // Do nothing if there is a known planned value. + if !req.PlanValue.IsUnknown() { + return + } + + // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. + if req.ConfigValue.IsUnknown() { + return + } + + resp.PlanValue = req.StateValue +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5e46b9f90..c3ffcffb9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# github.com/PagerDuty/go-pagerduty v1.8.1-0.20241111225923-0ef8f340dd3c +# github.com/PagerDuty/go-pagerduty v1.8.1-0.20250113202017-9831333ebe6b ## explicit; go 1.19 github.com/PagerDuty/go-pagerduty # github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c @@ -165,6 +165,7 @@ github.com/hashicorp/terraform-plugin-framework/providerserver github.com/hashicorp/terraform-plugin-framework/resource github.com/hashicorp/terraform-plugin-framework/resource/schema github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault +github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier @@ -260,7 +261,7 @@ github.com/hashicorp/terraform-svchost # github.com/hashicorp/yamux v0.1.1 ## explicit; go 1.15 github.com/hashicorp/yamux -# github.com/heimweh/go-pagerduty v0.0.0-20240731213000-b0991665ac52 +# github.com/heimweh/go-pagerduty v0.0.0-20250113182705-ce1f94dc30af ## explicit; go 1.17 github.com/heimweh/go-pagerduty/pagerduty github.com/heimweh/go-pagerduty/persistentconfig diff --git a/website/docs/r/incident_type.html.markdown b/website/docs/r/incident_type.html.markdown new file mode 100644 index 000000000..abc919994 --- /dev/null +++ b/website/docs/r/incident_type.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "pagerduty" +page_title: "PagerDuty: pagerduty_incident_type" +sidebar_current: "docs-pagerduty-resource-incident_type" +description: |- + Creates and manages a incident_type in PagerDuty. +--- + +# pagerduty\_incident\_type + +An [incident\_type](https://developer.pagerduty.com/api-reference/1981087c1914c-create-an-incident-type) +is a feature which allows customers to categorize incidents, such as a security +incident, a major incident, or a fraud incident. + + + + +## Example Usage + +```hcl +data "pagerduty_incident_type" "base" { + display_name = "Base Incident" +} + +resource "pagerduty_incident_type" "example" { + name = "backoffice" + display_name = "Backoffice Incident" + parent_type = data.pagerduty_incident_type.base.id + description = "Internal incidents not facing customer" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Incident Type. Usage of the suffix `_default` is prohibited. This cannot be changed once the incident type has been created. +* `display_name` - (Required) The display name of the Incident Type. Usage of the prefixes PD, PagerDuty, or the suffixes Default, or (Default) is prohibited. +* `parent_type` - (Required) The parent type of the Incident Type. Either name or id of the parent type can be used. +* `description` - A succinct description of the Incident Type. +* `enabled` - State of this Incident Type object. Defaults to true if not provided. + +## Attributes Reference + +* `id` - The unique identifier of the incident type. +* `type` - A string that determines the schema of the object. + +## Import + +Services can be imported using the `id`, e.g. + +``` +$ terraform import pagerduty_incident_type.main P12345 +``` diff --git a/website/docs/r/incident_type_custom_field.html.markdown b/website/docs/r/incident_type_custom_field.html.markdown new file mode 100644 index 000000000..fe79bf489 --- /dev/null +++ b/website/docs/r/incident_type_custom_field.html.markdown @@ -0,0 +1,81 @@ +--- +layout: "pagerduty" +page_title: "PagerDuty: pagerduty_incident_type_custom_field" +sidebar_current: "docs-pagerduty-resource-incident-type-custom-field" +description: |- + Creates and manages a incident type custom field in PagerDuty. +--- + +# pagerduty\_incident\_type\_custom\_field + +An [incident type custom fields](https://developer.pagerduty.com/api-reference/423b6701f3f1b-create-a-custom-field-for-an-incident-type) +are a feature which will allow customers to extend Incidents with their own +custom data, to provide additional context and support features such as +customized filtering, search and analytics. Custom Fields can be applied to +different incident types. Child types will inherit custom fields from their +parent types. + + +## Example Usage + +```hcl +resource "pagerduty_incident_type_custom_field" "alarm_time" { + name = "alarm_time_minutes" + display_name = "Alarm Time" + data_type = "integer" + field_type = "single_value" + default_value = jsonencode(5) + incident_type = "incident_default" +} + +data "pagerduty_incident_type" "foo" { + display_name = "Foo" +} + +resource "pagerduty_incident_type_custom_field" "level" { + name = "level" + incident_type = data.pagerduty_incident_type.foo.id + display_name = "Level" + data_type = "string" + field_type = "single_value_fixed" + field_options = ["Trace", "Debug", "Info", "Warn", "Error", "Fatal"] +} + +resource "pagerduty_incident_type_custom_field" "cs_impact" { + name = "impact" + incident_type = data.pagerduty_incident_type.foo.id + display_name = "Customer Impact" + data_type = "string" + field_type = "multi_value" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) [Updating causes resource replacement] The name of the custom field. +* `incident_type` - (Required) [Updating causes resource replacement] The id of the incident type the custom field is associated with. +* `display_name` - (Required) The display name of the custom Type. +* `data_type` - (Required) [Updating causes resource replacement] The type of the data of this custom field. Can be one of `string`, `integer`, `float`, `boolean`, `datetime`, or `url` when `data_type` is `single_value`, otherwise must be `string`. Update +* `field_type` - (Required) [Updating causes resource replacement] The field type of the field. Must be one of `single_value`, `single_value_fixed`, `multi_value`, or `multi_value_fixed`. +* `description` - The description of the custom field. +* `default_value` - The default value to set when new incidents are created. Always specified as a string. +* `enabled` - Whether the custom field is enabled. Defaults to true if not provided. +* `field_options` - The options for the custom field. Can only be applied to fields with a type of `single_value_fixed` or `multi_value_fixed`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the field. +* `self` - The API show URL at which the object is accessible. +* `summary` - A short-form, server-generated string that provides succinct, important information about an object suitable for primary labeling of an entity in a client. In many cases, this will be identical to name, though it is not intended to be an identifier. + +## Import + +Fields can be imported using the combination of `incident_type_id` and `field_id`, e.g. + +``` +$ terraform import pagerduty_incident_custom_field.cs_impact PT1234:PF1234 +```