From 1e532762f3335a25fb9a560dd4e70cf878e4d15b Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Thu, 8 Feb 2024 12:36:00 -0300 Subject: [PATCH] Migrate resource service Using protocol v5 --- pagerduty/provider.go | 1 - pagerdutyplugin/provider.go | 1 + pagerdutyplugin/resource_pagerduty_service.go | 908 ++++++++++++++++++ .../resource_pagerduty_service_test.go | 27 + util/build.go | 23 + util/validator.go | 184 ++++ .../int64validator/all.go | 57 ++ .../int64validator/also_requires.go | 26 + .../int64validator/any.go | 65 ++ .../int64validator/any_with_all_warnings.go | 67 ++ .../int64validator/at_least.go | 58 ++ .../int64validator/at_least_one_of.go | 27 + .../int64validator/at_least_sum_of.go | 116 +++ .../int64validator/at_most.go | 58 ++ .../int64validator/at_most_sum_of.go | 116 +++ .../int64validator/between.go | 63 ++ .../int64validator/conflicts_with.go | 27 + .../int64validator/doc.go | 5 + .../int64validator/equal_to_product_of.go | 116 +++ .../int64validator/equal_to_sum_of.go | 116 +++ .../int64validator/exactly_one_of.go | 28 + .../int64validator/none_of.go | 65 ++ .../int64validator/one_of.go | 63 ++ vendor/modules.txt | 1 + 24 files changed, 2217 insertions(+), 1 deletion(-) create mode 100644 pagerdutyplugin/resource_pagerduty_service.go create mode 100644 pagerdutyplugin/resource_pagerduty_service_test.go create mode 100644 util/build.go create mode 100644 util/validator.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/all.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/also_requires.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any_with_all_warnings.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_one_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_sum_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most_sum_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/between.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/conflicts_with.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_product_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_sum_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/exactly_one_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/none_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/one_of.go diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 3d5cf8799..203e6047a 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -113,7 +113,6 @@ func Provider(isMux bool) *schema.Provider { "pagerduty_escalation_policy": resourcePagerDutyEscalationPolicy(), "pagerduty_maintenance_window": resourcePagerDutyMaintenanceWindow(), "pagerduty_schedule": resourcePagerDutySchedule(), - "pagerduty_service": resourcePagerDutyService(), "pagerduty_service_integration": resourcePagerDutyServiceIntegration(), "pagerduty_team": resourcePagerDutyTeam(), "pagerduty_team_membership": resourcePagerDutyTeamMembership(), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 14163ae85..d082e9f7e 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -60,6 +60,7 @@ func (p *Provider) Resources(ctx context.Context) [](func() resource.Resource) { return [](func() resource.Resource){ func() resource.Resource { return &resourceBusinessService{} }, func() resource.Resource { return &resourceServiceDependency{} }, + func() resource.Resource { return &resourceService{} }, } } diff --git a/pagerdutyplugin/resource_pagerduty_service.go b/pagerdutyplugin/resource_pagerduty_service.go new file mode 100644 index 000000000..01d68bf6e --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_service.go @@ -0,0 +1,908 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "strconv" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "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/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "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" + "github.com/terraform-providers/terraform-provider-pagerduty/util" +) + +type resourceService struct { + client *pagerduty.Client +} + +var ( + _ resource.ResourceWithConfigure = (*resourceService)(nil) + _ resource.ResourceWithImportState = (*resourceService)(nil) +) + +func (r *resourceService) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceService) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_service" +} + +func (r *resourceService) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + util.IsAllowedStringValidator(util.NoNonPrintableChars), + }, + }, + + "acknowledgement_timeout": schema.StringAttribute{ + Computed: true, + Optional: true, + Default: stringdefault.StaticString("1800"), + }, + + "alert_creation": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("create_incidents"), + Validators: []validator.String{ + stringvalidator.OneOf("create_alerts_and_incidents", "create_incidents"), + }, + }, + + "alert_grouping": schema.StringAttribute{ + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("time", "intelligent", "rules"), + stringvalidator.ConflictsWith(path.MatchRoot("alert_grouping_parameters")), + }, + DeprecationMessage: "Use `alert_grouping_parameters.type`", + }, + + "alert_grouping_timeout": schema.StringAttribute{ + Computed: true, + Optional: true, + DeprecationMessage: "Use `alert_grouping_parameters.config.timeout`", + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("alert_grouping_parameters")), + }, + }, + + "auto_resolve_timeout": schema.StringAttribute{ + Computed: true, + Optional: true, + Default: stringdefault.StaticString("14400"), + }, + + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("Managed by Terraform"), + }, + + "id": schema.StringAttribute{Computed: true}, + "created_at": schema.StringAttribute{Computed: true}, + "escalation_policy": schema.StringAttribute{Required: true}, + "html_url": schema.StringAttribute{Computed: true}, + "last_incident_timestamp": schema.StringAttribute{Computed: true}, + "response_play": schema.StringAttribute{Computed: true, Optional: true}, + "status": schema.StringAttribute{Computed: true}, + "type": schema.StringAttribute{Computed: true}, + + "alert_grouping_parameters": schema.ListAttribute{ + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ConflictsWith(path.MatchRoot("alert_grouping")), + listvalidator.ConflictsWith(path.MatchRoot("alert_grouping_timeout")), + // util.ValidateAlertGroupingParametersType("time", "intelligent", "rules"), + // util.ValidateAlertGroupingParametersConfigAggregate("all", "any"), + // util.ValidateAlertGroupingParametersConfigTimeWindow(300, 3600), + }, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + // Validators: []validator.String{ stringvalidator.OneOf("time", "intelligent", "rules"), }, + "config": types.ListType{ + // Validators: []validator.List{listvalidator.SizeAtMost(1)}, + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "timeout": types.Int64Type, + "fields": types.ListType{ElemType: types.StringType}, + "aggregate": types.StringType, + // Validators: []validator.String{ stringvalidator.OneOf("all", "any"), }, + "time_window": types.Int64Type, + // Validators: []validator.Int64{ int64validator.Between(300, 3600), }, + }, + }, + }, + }, + }, + }, + + "auto_pause_notifications_parameters": schema.ListAttribute{ + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "enabled": types.BoolType, + "timeout": types.Int64Type, + // Validators: []validator.Int64{ int64validator.OneOf(120, 180, 300, 600, 900), }, + }, + }, + }, + + "incident_urgency_rule": schema.ListAttribute{ + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, // required + "urgency": types.StringType, + "during_support_hours": types.ListType{ + // Validators: []validator.List{listvalidator.SizeBetween(1, 1)}, + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, // require + "urgency": types.StringType, + }, + }, + }, + "outside_support_hours": types.ListType{ + // Validators: []validator.List{ listvalidator.SizeBetween(1, 1), }, + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, // require + "urgency": types.StringType, + }, + }, + }, + }, + }, + }, + + "scheduled_actions": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "to_urgency": types.StringType, + "at": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "name": types.StringType, + }, + }, + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 1), + }, + }, + + "support_hours": schema.ListAttribute{ + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "start_time": types.StringType, + "end_time": types.StringType, + "time_zone": types.StringType, + // Validators: []validator.String{util.ValidateTimezone()}, + "days_of_week": types.ListType{ + // Validators: []validator.List{ listvalidator.SizeAtMost(7), }, + ElemType: types.StringType, + }, + }, + }, + }, + }, + } +} + +func (r *resourceService) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceService) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceServiceModel + if d := req.Plan.Get(ctx, &model); d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + serviceBody := buildService(ctx, &model, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Creating PagerDuty service %s", serviceBody.Name) + + service, err := r.client.CreateServiceWithContext(ctx, serviceBody) + if err != nil { + resp.Diagnostics.AddError("Error calling CreateServiceWithContext", err.Error()) + return + } + + log.Printf("[CG] %#v", service) + + err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + serviceResponse, err := r.client.GetServiceWithContext(ctx, service.ID, &pagerduty.GetServiceOptions{ + Includes: []string{"auto_pause_notifications_parameters"}, + }) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + log.Printf("[CG] serviceResponse: %#v", serviceResponse) + model = flattenService(ctx, serviceResponse, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return retry.NonRetryableError(fmt.Errorf("%#v", resp.Diagnostics)) + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError("Error calling GetServiceWithContext", err.Error()) + return + } + + log.Printf("[CG] Model: %#v", service) + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceService) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id types.String + if d := req.State.GetAttribute(ctx, path.Root("id"), &id); d.HasError() { + resp.Diagnostics.Append(d...) + } + log.Printf("[INFO] Reading PagerDuty service %s", id) + + if id.IsNull() { + resp.State.RemoveResource(ctx) + return + } + + var model resourceServiceModel + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + serviceResponse, err := r.client.GetServiceWithContext(ctx, id.ValueString(), &pagerduty.GetServiceOptions{ + Includes: []string{"auto_pause_notifications_parameters"}, + }) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return nil + } + return retry.RetryableError(err) + } + model = flattenService(ctx, serviceResponse, &resp.Diagnostics) + return nil + }) + if err != nil { + resp.Diagnostics.AddError("Error calling GetServiceWithContext", err.Error()) + return + } + resp.State.Set(ctx, &model) +} + +func (r *resourceService) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r *resourceService) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var id types.String + if d := req.State.GetAttribute(ctx, path.Root("id"), &id); d.HasError() { + resp.Diagnostics.Append(d...) + } + log.Printf("[INFO] Deleting PagerDuty service %s", id) + + if id.IsNull() { + resp.State.RemoveResource(ctx) + return + } + + if err := r.client.DeleteServiceWithContext(ctx, id.ValueString()); err != nil { + resp.Diagnostics.AddError("Error calling DeleteServiceWithContext", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +type resourceServiceModel struct { + ID types.String `tfsdk:"id"` + AcknowledgementTimeout types.String `tfsdk:"acknowledgement_timeout"` + AlertCreation types.String `tfsdk:"alert_creation"` + AlertGrouping types.String `tfsdk:"alert_grouping"` + AlertGroupingTimeout types.String `tfsdk:"alert_grouping_timeout"` + AutoResolveTimeout types.String `tfsdk:"auto_resolve_timeout"` + CreatedAt types.String `tfsdk:"created_at"` + Description types.String `tfsdk:"description"` + EscalationPolicy types.String `tfsdk:"escalation_policy"` + HtmlUrl types.String `tfsdk:"html_url"` + LastIncidentTimestamp types.String `tfsdk:"last_incident_timestamp"` + Name types.String `tfsdk:"name"` + ResponsePlay types.String `tfsdk:"response_play"` + Status types.String `tfsdk:"status"` + Type types.String `tfsdk:"type"` + AlertGroupingParameters types.List `tfsdk:"alert_grouping_parameters"` + AutoPauseNotificationsParameters types.List `tfsdk:"auto_pause_notifications_parameters"` + IncidentUrgencyRule types.List `tfsdk:"incident_urgency_rule"` + ScheduledActions types.List `tfsdk:"scheduled_actions"` + SupportHours types.List `tfsdk:"support_hours"` +} + +func buildService(ctx context.Context, model *resourceServiceModel, diags *diag.Diagnostics) pagerduty.Service { + service := pagerduty.Service{ + Name: model.Name.ValueString(), + Description: model.Description.ValueString(), + AlertCreation: model.AlertCreation.ValueString(), + AlertGrouping: model.AlertGrouping.ValueString(), + } + + u := util.StringToUintPointer(path.Root("auto_resolve_timeout"), model.AutoResolveTimeout, diags) + service.AutoResolveTimeout = u + + u = util.StringToUintPointer(path.Root("acknowledgement_timeout"), model.AcknowledgementTimeout, diags) + service.AcknowledgementTimeout = u + + u = util.StringToUintPointer(path.Root("alert_grouping_timeout"), model.AlertGroupingTimeout, diags) + service.AlertGroupingTimeout = u + + service.EscalationPolicy.ID = model.EscalationPolicy.ValueString() + service.EscalationPolicy.Type = "escalation_policy_reference" + + service.AlertGroupingParameters = buildAlertGroupingParameters(ctx, model.AlertGroupingParameters, diags) + service.AutoPauseNotificationsParameters = buildAutoPauseNotificationsParameters(ctx, model.AutoPauseNotificationsParameters, diags) + service.IncidentUrgencyRule = buildIncidentUrgencyRule(ctx, model.IncidentUrgencyRule, diags) + service.ScheduledActions = buildScheduledActions(ctx, model.ScheduledActions, diags) + service.SupportHours = buildSupportHours(ctx, model.SupportHours, diags) + + if !model.ResponsePlay.IsNull() && !model.ResponsePlay.IsUnknown() { + service.ResponsePlay = &pagerduty.APIObject{ + ID: model.ResponsePlay.ValueString(), + Type: "response_play_reference", + } + } + + return service +} + +func buildAlertGroupingParameters(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.AlertGroupingParameters { + if list.IsNull() || list.IsUnknown() { + return nil + } + var target []struct { + Type types.String `tfsdk:"type"` + Config types.List `tfsdk:"config"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return nil + } + obj := target[0] + return &pagerduty.AlertGroupingParameters{ + Type: obj.Type.ValueString(), + Config: buildAlertGroupingConfig(ctx, obj.Config, diags), + } +} + +func buildAlertGroupingConfig(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.AlertGroupParamsConfig { + var target []struct { + Timeout types.Int64 `tfsdk:"timeout"` + Aggregate types.String `tfsdk:"aggregate"` + Fields types.List `tfsdk:"fields"` + TimeWindow types.Int64 `tfsdk:"time_window"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return nil + } + obj := target[0] + + ut := uint(obj.Timeout.ValueInt64()) + + var fields []string + if d := obj.Fields.ElementsAs(ctx, &fields, false); d.HasError() { + diags.Append(d...) + return nil + } + + return &pagerduty.AlertGroupParamsConfig{ + Timeout: &ut, + Aggregate: obj.Aggregate.ValueString(), + Fields: fields, + } +} + +func buildAutoPauseNotificationsParameters(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.AutoPauseNotificationsParameters { + var target []struct { + Timeout types.Int64 `tfsdk:"timeout"` + Enabled types.Bool `tfsdk:"enabled"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return nil + } + obj := target[0] + + return &pagerduty.AutoPauseNotificationsParameters{ + Enabled: obj.Enabled.ValueBool(), + Timeout: uint(obj.Timeout.ValueInt64()), + } +} + +func buildIncidentUrgencyRule(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.IncidentUrgencyRule { + if list.IsNull() || list.IsUnknown() { + return nil + } + var target []struct { + Type types.String `tfsdk:"type"` + Urgency types.String `tfsdk:"urgency"` + DuringSupportHours types.List `tfsdk:"during_support_hours"` + OutsideSupportHours types.List `tfsdk:"outside_support_hours"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return nil + } + obj := target[0] + incidentUrgencyRule := &pagerduty.IncidentUrgencyRule{ + Type: obj.Type.ValueString(), + Urgency: obj.Urgency.ValueString(), + } + incidentUrgencyRule.DuringSupportHours = buildIncidentUrgencyType(ctx, obj.DuringSupportHours, diags) + incidentUrgencyRule.OutsideSupportHours = buildIncidentUrgencyType(ctx, obj.OutsideSupportHours, diags) + return incidentUrgencyRule +} + +func buildIncidentUrgencyType(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.IncidentUrgencyType { + var target []struct { + Type types.String `tfsdk:"type"` + Urgency types.String `tfsdk:"urgency"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + } + obj := target[0] + return &pagerduty.IncidentUrgencyType{ + Type: obj.Type.ValueString(), + Urgency: obj.Urgency.ValueString(), + } +} + +func buildScheduledActions(ctx context.Context, list types.List, diags *diag.Diagnostics) []pagerduty.ScheduledAction { + if list.IsNull() || list.IsUnknown() { + return nil + } + var target []struct { + Type types.String `tfsdk:"type"` + ToUrgency types.String `tfsdk:"to_urgency"` + At types.List `tfsdk:"at"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + } + scheduledActions := []pagerduty.ScheduledAction{} + for _, src := range target { + dst := pagerduty.ScheduledAction{ + Type: src.Type.ValueString(), + ToUrgency: src.ToUrgency.ValueString(), + At: buildScheduledActionAt(ctx, src.At, diags), + } + scheduledActions = append(scheduledActions, dst) + } + return scheduledActions +} + +func buildScheduledActionAt(ctx context.Context, list types.List, diags *diag.Diagnostics) pagerduty.InlineModel { + var target []struct { + Type types.String `tfsdk:"type"` + Name types.String `tfsdk:"name"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return pagerduty.InlineModel{} + } + obj := target[0] + return pagerduty.InlineModel{ + Type: obj.Type.ValueString(), + Name: obj.Name.ValueString(), + } +} + +func buildSupportHours(ctx context.Context, list types.List, diags *diag.Diagnostics) *pagerduty.SupportHours { + if list.IsNull() || list.IsUnknown() { + return nil + } + var target []struct { + Type types.String `tfsdk:"type"` + Timezone types.String `tfsdk:"time_zone"` + StartTime types.String `tfsdk:"start_time"` + EndTime types.String `tfsdk:"end_time"` + DaysOfWeek types.List `tfsdk:"days_of_week"` + } + if d := list.ElementsAs(ctx, &target, false); d.HasError() { + diags.Append(d...) + return nil + } + obj := target[0] + supportHours := &pagerduty.SupportHours{ + Type: obj.Type.ValueString(), + Timezone: obj.Timezone.ValueString(), + StartTime: obj.StartTime.ValueString(), + EndTime: obj.EndTime.ValueString(), + } + + if !obj.DaysOfWeek.IsNull() { + daysOfWeekStr := []string{} + log.Printf("[CG] %#v", obj.DaysOfWeek) + if d := obj.DaysOfWeek.ElementsAs(ctx, &daysOfWeekStr, false); d.HasError() { + diags.Append(d...) + return nil + } + daysOfWeek := make([]uint, 0, len(daysOfWeekStr)) + for _, s := range daysOfWeekStr { + v, err := strconv.Atoi(s) + if err != nil { + continue + } + daysOfWeek = append(daysOfWeek, uint(v)) + } + supportHours.DaysOfWeek = daysOfWeek + } + return supportHours +} + +func flattenService(ctx context.Context, service *pagerduty.Service, diags *diag.Diagnostics) resourceServiceModel { + model := resourceServiceModel{ + ID: types.StringValue(service.ID), + AlertCreation: types.StringValue(service.AlertCreation), + CreatedAt: types.StringValue(service.CreateAt), + Description: types.StringValue(service.Description), + EscalationPolicy: types.StringValue(service.EscalationPolicy.ID), + HtmlUrl: types.StringValue(service.HTMLURL), + LastIncidentTimestamp: types.StringValue(service.LastIncidentTimestamp), + Name: types.StringValue(service.Name), + Status: types.StringValue(service.Status), + Type: types.StringValue(service.Type), + } + + if service.AcknowledgementTimeout != nil { + s := strconv.Itoa(int(*service.AcknowledgementTimeout)) + model.AcknowledgementTimeout = types.StringValue(s) + } + + if service.AutoResolveTimeout != nil { + s := strconv.Itoa(int(*service.AutoResolveTimeout)) + model.AutoResolveTimeout = types.StringValue(s) + } + + if service.AlertGrouping != "" { + model.AlertGrouping = types.StringValue(service.AlertGrouping) + } + + if service.AlertGroupingTimeout != nil { + s := strconv.Itoa(int(*service.AlertGroupingTimeout)) + model.AlertGroupingTimeout = types.StringValue(s) + } + + model.AlertGroupingParameters = flattenAlertGroupingParameters(ctx, service.AlertGroupingParameters, diags) + model.AutoPauseNotificationsParameters = flattenAutoPauseNotificationsParameters(service.AutoPauseNotificationsParameters, diags) + model.IncidentUrgencyRule = flattenIncidentUrgencyRule(service.IncidentUrgencyRule, diags) + + if service.ResponsePlay != nil { + model.ResponsePlay = types.StringValue(service.ResponsePlay.ID) + } + + model.ScheduledActions = flattenScheduledActions(service.ScheduledActions, diags) + model.SupportHours = flattenSupportHours(service.SupportHours, diags) + + return model +} + +func flattenAlertGroupingParameters(ctx context.Context, params *pagerduty.AlertGroupingParameters, diags *diag.Diagnostics) types.List { + alertGroupParamsConfigObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "aggregate": types.StringType, + "fields": types.ListType{ElemType: types.StringType}, + "timeout": types.Int64Type, + "time_window": types.Int64Type, + }, + } + alertGroupingParametersObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "config": types.ListType{ElemType: alertGroupParamsConfigObjectType}, + }, + } + nullList := types.ListNull(alertGroupingParametersObjectType) + if params == nil { + return nullList + } + + configList := types.ListNull(alertGroupParamsConfigObjectType) + if params.Config != nil { + fieldsList, d := types.ListValueFrom(ctx, types.StringType, params.Config.Fields) + if d.HasError() { + diags.Append(d...) + return nullList + } + + var timeout types.Int64 + if params.Config.Timeout != nil { + timeout = types.Int64Value(int64(*params.Config.Timeout)) + } + + aggregate := types.StringNull() + if params.Config.Aggregate != "" { + aggregate = types.StringValue(params.Config.Aggregate) + } + + configObj, d := types.ObjectValue(alertGroupParamsConfigObjectType.AttrTypes, map[string]attr.Value{ + "aggregate": aggregate, + "fields": fieldsList, + "timeout": timeout, + "time_window": types.Int64Null(), // TODO + }) + if d.HasError() { + diags.Append(d...) + return nullList + } + configList, d = types.ListValue(alertGroupParamsConfigObjectType, []attr.Value{configObj}) + if d.HasError() { + diags.Append(d...) + return nullList + } + } + + obj, d := types.ObjectValue(alertGroupingParametersObjectType.AttrTypes, map[string]attr.Value{ + "type": types.StringValue(params.Type), + "config": configList, + }) + if d.HasError() { + diags.Append(d...) + return nullList + } + + return types.ListValueMust(alertGroupingParametersObjectType, []attr.Value{obj}) +} + +func flattenAutoPauseNotificationsParameters(params *pagerduty.AutoPauseNotificationsParameters, diags *diag.Diagnostics) types.List { + autoPauseNotificationsParametersObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "enabled": types.BoolType, + "timeout": types.Int64Type, + }, + } + nullList := types.ListNull(autoPauseNotificationsParametersObjectType) + if params == nil { + return nullList + } + + timeout := types.Int64Null() + if params.Enabled { + timeout = types.Int64Value(int64(params.Timeout)) + } + + obj, d := types.ObjectValue(autoPauseNotificationsParametersObjectType.AttrTypes, map[string]attr.Value{ + "enabled": types.BoolValue(params.Enabled), + "timeout": timeout, + }) + if d.HasError() { + diags.Append(d...) + return nullList + } + + list, d := types.ListValue(autoPauseNotificationsParametersObjectType, []attr.Value{obj}) + if d.HasError() { + diags.Append(d...) + return nullList + } + + return list +} + +var incidentUrgencyTypeObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "urgency": types.StringType, + }, +} + +func flattenIncidentUrgencyRule(rule *pagerduty.IncidentUrgencyRule, diags *diag.Diagnostics) types.List { + incidentUrgencyRuleObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "urgency": types.StringType, + "during_support_hours": types.ListType{ElemType: incidentUrgencyTypeObjectType}, + "outside_support_hours": types.ListType{ElemType: incidentUrgencyTypeObjectType}, + }, + } + nullList := types.ListNull(incidentUrgencyTypeObjectType) + if rule == nil { + return nullList + } + + objValues := map[string]attr.Value{ + "type": types.StringValue(rule.Type), + "urgency": types.StringNull(), + "during_support_hours": types.ListNull(incidentUrgencyTypeObjectType), + "outside_support_hours": types.ListNull(incidentUrgencyTypeObjectType), + } + if rule.Urgency != "" { + objValues["urgency"] = types.StringValue(rule.Urgency) + } + if rule.DuringSupportHours != nil { + objValues["during_support_hours"] = flattenIncidentUrgencyType(rule.DuringSupportHours, diags) + } + if rule.OutsideSupportHours != nil { + objValues["outside_support_hours"] = flattenIncidentUrgencyType(rule.OutsideSupportHours, diags) + } + if diags.HasError() { + return nullList + } + + obj, d := types.ObjectValue(incidentUrgencyRuleObjectType.AttrTypes, objValues) + if d.HasError() { + diags.Append(d...) + return nullList + } + + list, d := types.ListValue(incidentUrgencyRuleObjectType, []attr.Value{obj}) + diags.Append(d...) + return list +} + +func flattenIncidentUrgencyType(urgency *pagerduty.IncidentUrgencyType, diags *diag.Diagnostics) types.List { + obj, d := types.ObjectValue(incidentUrgencyTypeObjectType.AttrTypes, map[string]attr.Value{ + "type": types.StringValue(urgency.Type), + "urgency": types.StringValue(urgency.Urgency), + }) + diags.Append(d...) + if d.HasError() { + return types.List{} + } + list, d := types.ListValue(incidentUrgencyTypeObjectType, []attr.Value{obj}) + diags.Append(d...) + return list +} + +var scheduledActionAtObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "name": types.StringType, + }, +} + +func flattenScheduledActions(actions []pagerduty.ScheduledAction, diags *diag.Diagnostics) types.List { + scheduledActionObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "to_urgency": types.StringType, + "at": types.ListType{ElemType: scheduledActionAtObjectType}, + }, + } + nullList := types.ListNull(scheduledActionObjectType) + if len(actions) == 0 { + return nullList + } + + elements := []attr.Value{} + for _, action := range actions { + obj, d := types.ObjectValue(scheduledActionObjectType.AttrTypes, map[string]attr.Value{ + "type": types.StringValue(action.Type), + "to_urgency": types.StringValue(action.ToUrgency), + "at": flattenScheduledActionAt(action.At, diags), + }) + diags.Append(d...) + if diags.HasError() { + return nullList + } + elements = append(elements, obj) + } + + list, d := types.ListValue(scheduledActionObjectType, elements) + diags.Append(d...) + return list +} + +func flattenScheduledActionAt(at pagerduty.InlineModel, diags *diag.Diagnostics) types.List { + obj, d := types.ObjectValue(scheduledActionAtObjectType.AttrTypes, map[string]attr.Value{ + "type": types.StringValue(at.Type), + "name": types.StringValue(at.Name), + }) + if d.HasError() { + diags.Append(d...) + return types.List{} + } + list, d := types.ListValue(scheduledActionAtObjectType, []attr.Value{obj}) + diags.Append(d...) + return list +} + +func flattenSupportHours(hours *pagerduty.SupportHours, diags *diag.Diagnostics) types.List { + supportHoursObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "start_time": types.StringType, + "end_time": types.StringType, + "time_zone": types.StringType, + "days_of_week": types.ListType{ElemType: types.StringType}, + }, + } + nullList := types.ListNull(supportHoursObjectType) + if hours == nil { + return nullList + } + + daysOfWeek := []attr.Value{} + for _, dow := range hours.DaysOfWeek { + v := strconv.FormatInt(int64(dow), 10) + daysOfWeek = append(daysOfWeek, types.StringValue(v)) + } + + dowList, d := types.ListValue(types.StringType, daysOfWeek) + diags.Append(d...) + + obj, d := types.ObjectValue(supportHoursObjectType.AttrTypes, map[string]attr.Value{ + "type": types.StringValue(hours.Type), + "start_time": types.StringValue(hours.StartTime), + "end_time": types.StringValue(hours.EndTime), + "time_zone": types.StringValue(hours.Timezone), + "days_of_week": dowList, + }) + if d.HasError() { + diags.Append(d...) + return nullList + } + + list, d := types.ListValue(supportHoursObjectType, []attr.Value{obj}) + diags.Append(d...) + return list +} diff --git a/pagerdutyplugin/resource_pagerduty_service_test.go b/pagerdutyplugin/resource_pagerduty_service_test.go new file mode 100644 index 000000000..99c8becb6 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_service_test.go @@ -0,0 +1,27 @@ +package pagerduty + +import ( + "context" + "testing" + + "github.com/PagerDuty/go-pagerduty" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +func TestFlattenAlertGroupingParameters_Basic(t *testing.T) { + var diags diag.Diagnostics + var timeout uint = 1000 + params := &pagerduty.AlertGroupingParameters{ + Type: "foo", + Config: &pagerduty.AlertGroupParamsConfig{ + Timeout: &timeout, + Aggregate: "aggregate", + Fields: []string{"a", "b", "c"}, + }, + } + li := flattenAlertGroupingParameters(context.Background(), params, &diags) + if diags.HasError() { + t.Fatalf("unexpected error %s", diags) + } + t.Logf("%#v", li) +} diff --git a/util/build.go b/util/build.go new file mode 100644 index 000000000..53a6675d8 --- /dev/null +++ b/util/build.go @@ -0,0 +1,23 @@ +package util + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func StringToUintPointer(p path.Path, s types.String, diags *diag.Diagnostics) *uint { + if s.IsNull() || s.IsUnknown() { + return nil + } + if val, err := strconv.Atoi(s.ValueString()); err == nil { + uintvalue := uint(val) + return &uintvalue + } else { + diags.AddError(fmt.Sprintf("Value for %q is not a valid number", p), err.Error()) + } + return nil +} diff --git a/util/validator.go b/util/validator.go new file mode 100644 index 000000000..64e6eb37b --- /dev/null +++ b/util/validator.go @@ -0,0 +1,184 @@ +package util + +import ( + "context" + "fmt" + "strings" + "time" + "unicode" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + v2diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type stringDescriber struct{ s string } + +func (d stringDescriber) MarkdownDescription(context.Context) string { return d.s } +func (d stringDescriber) Description(ctx context.Context) string { return d.MarkdownDescription(ctx) } + +type timezoneValidator struct{ stringDescriber } + +func ValidateTimezone() validator.String { + return &timezoneValidator{stringDescriber{"checks time zone is supported by the machine's tzdata"}} +} + +func (v timezoneValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() { + return + } + value := req.ConfigValue.ValueString() + _, err := time.LoadLocation(value) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.Path, fmt.Sprintf("Timezone %q is invalid", value), err.Error(), + ) + } +} + +type validateIsAllowedString struct { + validateFn func(s string) bool + stringDescriber +} + +func (v validateIsAllowedString) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if ok := v.validateFn(req.ConfigValue.ValueString()); !ok { + resp.Diagnostics.AddError(v.stringDescriber.s, "") + } +} + +func IsAllowedStringValidator(mode StringContentValidationMode) validator.String { + switch mode { + case NoNonPrintableChars: + return validateIsAllowedString{ + func(s string) bool { + for _, char := range s { + if !unicode.IsPrint(char) { + return false + } + } + return s != "" && !strings.HasSuffix(s, " ") + }, + stringDescriber{"Name can not be blank, nor contain non-printable characters. Trailing white spaces are not allowed either."}, + } + default: + return validateIsAllowedString{ + func(s string) bool { return false }, + stringDescriber{"Invalid mode while using func IsAllowedStringValidator(mode StringContentValidationMode)"}, + } + } +} + +// ValidateIsAllowedString will always validate if string provided is not empty, +// neither has trailing white spaces. Additionally the string content validation +// will be done based on the `mode` set. +// +// mode: NoContentValidation | NoNonPrintableChars | NoNonPrintableCharsOrSpecialChars +func ReValidateIsAllowedString(mode StringContentValidationMode) schema.SchemaValidateDiagFunc { + return func(v interface{}, p cty.Path) v2diag.Diagnostics { + var diags v2diag.Diagnostics + + fillDiags := func() { + summary := "Name can not be blank. Trailing white spaces are not allowed either." + switch mode { + case NoNonPrintableChars: + summary = "Name can not be blank, nor contain non-printable characters. Trailing white spaces are not allowed either." + case NoNonPrintableCharsOrSpecialChars: + summary = "Name can not be blank, nor contain the characters '\\', '/', '&', '<', '>', or any non-printable characters. Trailing white spaces are not allowed either." + } + diags = append(diags, v2diag.Diagnostic{ + Severity: v2diag.Error, + Summary: summary, + AttributePath: p, + }) + } + + value := v.(string) + if value == "" { + fillDiags() + return diags + } + + for _, char := range value { + if (mode == NoNonPrintableChars || mode == NoNonPrintableCharsOrSpecialChars) && !unicode.IsPrint(char) { + fillDiags() + return diags + } + if mode == NoNonPrintableCharsOrSpecialChars { + switch char { + case '\\', '/', '&', '<', '>': + fillDiags() + return diags + } + } + } + + if strings.HasSuffix(value, " ") { + fillDiags() + return diags + } + + return diags + } +} + +type alertGroupingParametersValidator struct { + stringDescriber + oneOfValidator validator.String +} + +func (v alertGroupingParametersValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + if len(req.ConfigValue.Elements()) < 1 { + resp.Diagnostics.AddError("Expecting at least one element for alert_grouping_parameters", "") + return + } + + var target []struct { + Type types.String + Config types.List + } + if d := req.ConfigValue.ElementsAs(ctx, &target, false); d.HasError() { + resp.Diagnostics.Append(d...) + return + } + obj := target[0] + + { + vreq := validator.StringRequest{ + Path: req.Path.AtName("type"), + Config: req.Config, + ConfigValue: obj.Type, + } + vresp := &validator.StringResponse{} + v.oneOfValidator.ValidateString(ctx, vreq, vresp) + resp.Diagnostics.Append(vresp.Diagnostics...) + } +} + +func /*DEBUG */ ValidateOneOf(value types.String, allowed ...string) (diags diag.Diagnostics) { + found := false + for _, a := range allowed { + if a == value.ValueString() { + found = true + break + } + } + if !found { + diags.AddError(fmt.Sprint("Expecting alert_grouping_parameters.type to be one of", allowed), "") + return + } + return +} + +func ValidateAlertGroupingParametersType(allowedTypes ...string) validator.List { + // TODO + // return &alertGroupingParametersValidator{stringDescriber{""}, allowedTypes} + return &alertGroupingParametersValidator{stringDescriber{""}, stringvalidator.OneOf(allowedTypes...)} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/all.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/all.go new file mode 100644 index 000000000..374bdbeaa --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/all.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// All returns a validator which ensures that any configured attribute value +// attribute value validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...validator.Int64) validator.Int64 { + return allValidator{ + validators: validators, + } +} + +var _ validator.Int64 = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []validator.Int64 +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v allValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + for _, subValidator := range v.validators { + validateResp := &validator.Int64Response{} + + subValidator.ValidateInt64(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/also_requires.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/also_requires.go new file mode 100644 index 000000000..6e6de2780 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/also_requires.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AlsoRequires checks that a set of path.Expression has a non-null value, +// if the current attribute also has a non-null value. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.RequiredTogether], +// [providervalidator.RequiredTogether], or [resourcevalidator.RequiredTogether] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute being +// validated. +func AlsoRequires(expressions ...path.Expression) validator.Int64 { + return schemavalidator.AlsoRequiresValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any.go new file mode 100644 index 000000000..a44d082a5 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...validator.Int64) validator.Int64 { + return anyValidator{ + validators: validators, + } +} + +var _ validator.Int64 = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []validator.Int64 +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v anyValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + for _, subValidator := range v.validators { + validateResp := &validator.Int64Response{} + + subValidator.ValidateInt64(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any_with_all_warnings.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any_with_all_warnings.go new file mode 100644 index 000000000..1cd3ee365 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/any_with_all_warnings.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...validator.Int64) validator.Int64 { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ validator.Int64 = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []validator.Int64 +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v anyWithAllWarningsValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &validator.Int64Response{} + + subValidator.ValidateInt64(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least.go new file mode 100644 index 000000000..092a94790 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = atLeastValidator{} + +// atLeastValidator validates that an integer Attribute's value is at least a certain value. +type atLeastValidator struct { + min int64 +} + +// Description describes the validation in plain text formatting. +func (validator atLeastValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be at least %d", validator.min) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v atLeastValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if request.ConfigValue.ValueInt64() < v.min { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + v.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// AtLeast returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is greater than or equal to the given minimum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtLeast(min int64) validator.Int64 { + return atLeastValidator{ + min: min, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_one_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_one_of.go new file mode 100644 index 000000000..cf59b99e4 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_one_of.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AtLeastOneOf checks that of a set of path.Expression, +// including the attribute this validator is applied to, +// at least one has a non-null value. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// Refer to [datasourcevalidator.AtLeastOneOf], +// [providervalidator.AtLeastOneOf], or [resourcevalidator.AtLeastOneOf] +// for declaring this type of validation outside the schema definition. +// +// Any relative path.Expression will be resolved using the attribute being +// validated. +func AtLeastOneOf(expressions ...path.Expression) validator.Int64 { + return schemavalidator.AtLeastOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_sum_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_sum_of.go new file mode 100644 index 000000000..a8bb10694 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_least_sum_of.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = atLeastSumOfValidator{} + +// atLeastSumOfValidator validates that an integer Attribute's value is at least the sum of one +// or more integer Attributes retrieved via the given path expressions. +type atLeastSumOfValidator struct { + attributesToSumPathExpressions path.Expressions +} + +// Description describes the validation in plain text formatting. +func (av atLeastSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, p := range av.attributesToSumPathExpressions { + attributePaths = append(attributePaths, p.String()) + } + + return fmt.Sprintf("value must be at least sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (av atLeastSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (av atLeastSumOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + // Ensure input path expressions resolution against the current attribute + expressions := request.PathExpression.MergeExpressions(av.attributesToSumPathExpressions...) + + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.Path) { + continue + } + + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + sumOfAttribs += attribToSum.ValueInt64() + } + } + + if request.ConfigValue.ValueInt64() < sumOfAttribs { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + av.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// AtLeastSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is at least the sum of the attributes retrieved via the given path expression(s). +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtLeastSumOf(attributesToSumPathExpressions ...path.Expression) validator.Int64 { + return atLeastSumOfValidator{attributesToSumPathExpressions} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most.go new file mode 100644 index 000000000..b564a6e5d --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = atMostValidator{} + +// atMostValidator validates that an integer Attribute's value is at most a certain value. +type atMostValidator struct { + max int64 +} + +// Description describes the validation in plain text formatting. +func (validator atMostValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be at most %d", validator.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v atMostValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if request.ConfigValue.ValueInt64() > v.max { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + v.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// AtMost returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is less than or equal to the given maximum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtMost(max int64) validator.Int64 { + return atMostValidator{ + max: max, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most_sum_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most_sum_of.go new file mode 100644 index 000000000..cfdf77104 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/at_most_sum_of.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = atMostSumOfValidator{} + +// atMostSumOfValidator validates that an integer Attribute's value is at most the sum of one +// or more integer Attributes retrieved via the given path expressions. +type atMostSumOfValidator struct { + attributesToSumPathExpressions path.Expressions +} + +// Description describes the validation in plain text formatting. +func (av atMostSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, p := range av.attributesToSumPathExpressions { + attributePaths = append(attributePaths, p.String()) + } + + return fmt.Sprintf("value must be at most sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (av atMostSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (av atMostSumOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + // Ensure input path expressions resolution against the current attribute + expressions := request.PathExpression.MergeExpressions(av.attributesToSumPathExpressions...) + + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.Path) { + continue + } + + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + sumOfAttribs += attribToSum.ValueInt64() + } + } + + if request.ConfigValue.ValueInt64() > sumOfAttribs { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + av.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// AtMostSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is at most the sum of the given attributes retrieved via the given path expression(s). +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func AtMostSumOf(attributesToSumPathExpressions ...path.Expression) validator.Int64 { + return atMostSumOfValidator{attributesToSumPathExpressions} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/between.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/between.go new file mode 100644 index 000000000..879aeff01 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/between.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = betweenValidator{} + +// betweenValidator validates that an integer Attribute's value is in a range. +type betweenValidator struct { + min, max int64 +} + +// Description describes the validation in plain text formatting. +func (validator betweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("value must be between %d and %d", validator.min, validator.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v betweenValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if request.ConfigValue.ValueInt64() < v.min || request.ConfigValue.ValueInt64() > v.max { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + v.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// Between returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is greater than or equal to the given minimum and less than or equal to the given maximum. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func Between(min, max int64) validator.Int64 { + if min > max { + return nil + } + + return betweenValidator{ + min: min, + max: max, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/conflicts_with.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/conflicts_with.go new file mode 100644 index 000000000..2a73e72a0 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/conflicts_with.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// ConflictsWith checks that a set of path.Expression, +// including the attribute the validator is applied to, +// do not have a value simultaneously. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.Conflicting], +// [providervalidator.Conflicting], or [resourcevalidator.Conflicting] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute being +// validated. +func ConflictsWith(expressions ...path.Expression) validator.Int64 { + return schemavalidator.ConflictsWithValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/doc.go new file mode 100644 index 000000000..0e65c174a --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package int64validator provides validators for types.Int64 attributes. +package int64validator diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_product_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_product_of.go new file mode 100644 index 000000000..75c23ec20 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_product_of.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = equalToProductOfValidator{} + +// equalToProductOfValidator validates that an integer Attribute's value equals the product of one +// or more integer Attributes retrieved via the given path expressions. +type equalToProductOfValidator struct { + attributesToMultiplyPathExpressions path.Expressions +} + +// Description describes the validation in plain text formatting. +func (av equalToProductOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, p := range av.attributesToMultiplyPathExpressions { + attributePaths = append(attributePaths, p.String()) + } + + return fmt.Sprintf("value must be equal to the product of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (av equalToProductOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (av equalToProductOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + // Ensure input path expressions resolution against the current attribute + expressions := request.PathExpression.MergeExpressions(av.attributesToMultiplyPathExpressions...) + + // Multiply the value of all the attributes involved, but only if they are all known. + productOfAttribs := int64(1) + for _, expression := range expressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.Path) { + continue + } + + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + return + } + + // We know there is a value, convert it to the expected type + var attribToMultiply types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToMultiply) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + productOfAttribs *= attribToMultiply.ValueInt64() + } + } + + if request.ConfigValue.ValueInt64() != productOfAttribs { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + av.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// EqualToProductOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is equal to the product of the given attributes retrieved via the given path expression(s). +// +// Validation is skipped if any null (unconfigured) and/or unknown (known after apply) values are present. +func EqualToProductOf(attributesToMultiplyPathExpressions ...path.Expression) validator.Int64 { + return equalToProductOfValidator{attributesToMultiplyPathExpressions} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_sum_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_sum_of.go new file mode 100644 index 000000000..cf322c76c --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/equal_to_sum_of.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = equalToSumOfValidator{} + +// equalToSumOfValidator validates that an integer Attribute's value equals the sum of one +// or more integer Attributes retrieved via the given path expressions. +type equalToSumOfValidator struct { + attributesToSumPathExpressions path.Expressions +} + +// Description describes the validation in plain text formatting. +func (av equalToSumOfValidator) Description(_ context.Context) string { + var attributePaths []string + for _, p := range av.attributesToSumPathExpressions { + attributePaths = append(attributePaths, p.String()) + } + + return fmt.Sprintf("value must be equal to the sum of %s", strings.Join(attributePaths, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (av equalToSumOfValidator) MarkdownDescription(ctx context.Context) string { + return av.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (av equalToSumOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + // Ensure input path expressions resolution against the current attribute + expressions := request.PathExpression.MergeExpressions(av.attributesToSumPathExpressions...) + + // Sum the value of all the attributes involved, but only if they are all known. + var sumOfAttribs int64 + for _, expression := range expressions { + matchedPaths, diags := request.Config.PathMatches(ctx, expression) + response.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(request.Path) { + continue + } + + // Get the value + var matchedValue attr.Value + diags := request.Config.GetAttribute(ctx, mp, &matchedValue) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + continue + } + + // We know there is a value, convert it to the expected type + var attribToSum types.Int64 + diags = tfsdk.ValueAs(ctx, matchedValue, &attribToSum) + response.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + sumOfAttribs += attribToSum.ValueInt64() + } + } + + if request.ConfigValue.ValueInt64() != sumOfAttribs { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + av.Description(ctx), + fmt.Sprintf("%d", request.ConfigValue.ValueInt64()), + )) + } +} + +// EqualToSumOf returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a number, which can be represented by a 64-bit integer. +// - Is equal to the sum of the given attributes retrieved via the given path expression(s). +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func EqualToSumOf(attributesToSumPathExpressions ...path.Expression) validator.Int64 { + return equalToSumOfValidator{attributesToSumPathExpressions} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/exactly_one_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/exactly_one_of.go new file mode 100644 index 000000000..9edfa4f28 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/exactly_one_of.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// ExactlyOneOf checks that of a set of path.Expression, +// including the attribute the validator is applied to, +// one and only one attribute has a value. +// It will also cause a validation error if none are specified. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.ExactlyOneOf], +// [providervalidator.ExactlyOneOf], or [resourcevalidator.ExactlyOneOf] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute being +// validated. +func ExactlyOneOf(expressions ...path.Expression) validator.Int64 { + return schemavalidator.ExactlyOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/none_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/none_of.go new file mode 100644 index 000000000..749fe5547 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/none_of.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = noneOfValidator{} + +// noneOfValidator validates that the value does not match one of the values. +type noneOfValidator struct { + values []types.Int64 +} + +func (v noneOfValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +func (v noneOfValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value must be none of: %q", v.values) +} + +func (v noneOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + request.Path, + v.Description(ctx), + value.String(), + )) + + break + } +} + +// NoneOf checks that the Int64 held in the attribute +// is none of the given `values`. +func NoneOf(values ...int64) validator.Int64 { + frameworkValues := make([]types.Int64, 0, len(values)) + + for _, value := range values { + frameworkValues = append(frameworkValues, types.Int64Value(value)) + } + + return noneOfValidator{ + values: frameworkValues, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/one_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/one_of.go new file mode 100644 index 000000000..3a1e1db48 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/int64validator/one_of.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" +) + +var _ validator.Int64 = oneOfValidator{} + +// oneOfValidator validates that the value matches one of expected values. +type oneOfValidator struct { + values []types.Int64 +} + +func (v oneOfValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +func (v oneOfValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value must be one of: %q", v.values) +} + +func (v oneOfValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + request.Path, + v.Description(ctx), + value.String(), + )) +} + +// OneOf checks that the Int64 held in the attribute +// is one of the given `values`. +func OneOf(values ...int64) validator.Int64 { + frameworkValues := make([]types.Int64, 0, len(values)) + + for _, value := range values { + frameworkValues = append(frameworkValues, types.Int64Value(value)) + } + + return oneOfValidator{ + values: frameworkValues, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 414048df9..ea78933a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -176,6 +176,7 @@ github.com/hashicorp/terraform-plugin-framework/types/basetypes # github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 ## explicit; go 1.19 github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag +github.com/hashicorp/terraform-plugin-framework-validators/int64validator github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator github.com/hashicorp/terraform-plugin-framework-validators/listvalidator github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator