diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 9dc0998a7..3d5cf8799 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -126,7 +126,6 @@ func Provider(isMux bool) *schema.Provider { "pagerduty_ruleset": resourcePagerDutyRuleset(), "pagerduty_ruleset_rule": resourcePagerDutyRulesetRule(), "pagerduty_business_service": resourcePagerDutyBusinessService(), - "pagerduty_service_dependency": resourcePagerDutyServiceDependency(), "pagerduty_response_play": resourcePagerDutyResponsePlay(), "pagerduty_tag": resourcePagerDutyTag(), "pagerduty_tag_assignment": resourcePagerDutyTagAssignment(), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index f7fef54e2..14163ae85 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -59,6 +59,7 @@ func (p *Provider) DataSources(ctx context.Context) [](func() datasource.DataSou func (p *Provider) Resources(ctx context.Context) [](func() resource.Resource) { return [](func() resource.Resource){ func() resource.Resource { return &resourceBusinessService{} }, + func() resource.Resource { return &resourceServiceDependency{} }, } } diff --git a/pagerdutyplugin/resource_pagerduty_service_dependency.go b/pagerdutyplugin/resource_pagerduty_service_dependency.go new file mode 100644 index 000000000..8cae554e3 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_service_dependency.go @@ -0,0 +1,518 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + "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/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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceServiceDependency struct { + client *pagerduty.Client +} + +var ( + _ resource.ResourceWithConfigure = (*resourceServiceDependency)(nil) + _ resource.ResourceWithImportState = (*resourceServiceDependency)(nil) +) + +func (r *resourceServiceDependency) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_service_dependency" +} + +func (r *resourceServiceDependency) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + supportingServiceBlockObject := schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + "business_service", + "business_service_reference", + "service", + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } + + dependencyServiceBlockObject := schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( + "business_service", + "business_service_reference", + "service", + "service_dependency", // TODO + "technical_service_reference", + ), + }, + }, + }, + } + + dependencyBlockObject := schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{Optional: true, Computed: true}, + }, + Blocks: map[string]schema.Block{ + "supporting_service": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + }, + NestedObject: supportingServiceBlockObject, + }, + "dependent_service": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + }, + NestedObject: dependencyServiceBlockObject, + }, + }, + } + + dependencyBlock := schema.ListNestedBlock{ + NestedObject: dependencyBlockObject, + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeBetween(1, 1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + } + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + }, + Blocks: map[string]schema.Block{ + "dependency": dependencyBlock, + }, + } +} + +func (r *resourceServiceDependency) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceServiceDependencyModel + + if diags := req.Plan.Get(ctx, &model); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serviceDependency, diags := buildServiceDependencyStruct(ctx, model) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + dependencies := &pagerduty.ListServiceDependencies{ + Relationships: []*pagerduty.ServiceDependency{serviceDependency}, + } + + // TODO: retry + resourceServiceDependencyMu.Lock() + list, err := r.client.AssociateServiceDependenciesWithContext(ctx, dependencies) + resourceServiceDependencyMu.Unlock() + if err != nil { + // TODO: if 400 NonRetryable + resp.Diagnostics.AddError("Error calling AssociateServiceDependenciesWithContext", err.Error()) + return + } + + model, diags = flattenServiceDependency(list.Relationships) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceServiceDependency) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var model resourceServiceDependencyModel + + if diags := req.State.Get(ctx, &model); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serviceDependency, diags := buildServiceDependencyStruct(ctx, model) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + log.Printf("Reading PagerDuty dependency %s", serviceDependency.ID) + + serviceDependency, diags = r.requestGetServiceDependency(ctx, serviceDependency.ID, serviceDependency.DependentService.ID, serviceDependency.DependentService.Type) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if serviceDependency == nil { + resp.State.RemoveResource(ctx) + return + } + + model, diags = flattenServiceDependency([]*pagerduty.ServiceDependency{serviceDependency}) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceServiceDependency) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddWarning("Update for service dependency has no effect", "") +} + +func (r *resourceServiceDependency) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var model resourceServiceDependencyModel + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + var dependencies []*resourceServiceDependencyItemModel + if d := model.Dependency.ElementsAs(ctx, &dependencies, false); d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + var dependents []types.Object + if d := dependencies[0].DependentService.ElementsAs(ctx, &dependents, false); d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + var dependent struct { + ID types.String `tfsdk:"id"` + Type types.String `tfsdk:"type"` + } + if d := dependents[0].As(ctx, &dependent, basetypes.ObjectAsOptions{}); d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + id := model.ID.ValueString() + depId := dependent.ID.ValueString() + rt := dependent.Type.ValueString() + log.Println("[CG]", id, depId, rt) + + // TODO: retry + serviceDependency, diags := r.requestGetServiceDependency(ctx, id, depId, rt) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if serviceDependency == nil { + resp.State.RemoveResource(ctx) + return + } + if serviceDependency.SupportingService != nil { + serviceDependency.SupportingService.Type = convertServiceDependencyType(serviceDependency.SupportingService.Type) + log.Println("[CG]", serviceDependency.SupportingService.Type) + } + if serviceDependency.DependentService != nil { + serviceDependency.DependentService.Type = convertServiceDependencyType(serviceDependency.DependentService.Type) + log.Println("[CG]", serviceDependency.DependentService.Type) + } + + list := &pagerduty.ListServiceDependencies{ + Relationships: []*pagerduty.ServiceDependency{serviceDependency}, + } + _, err := r.client.DisassociateServiceDependenciesWithContext(ctx, list) + if err != nil { + diags.AddError("Error calling DisassociateServiceDependenciesWithContext", err.Error()) + return + } + + resp.State.RemoveResource(ctx) + return +} + +// requestGetServiceDependency requests the list of service dependencies +// according to its resource type, then searches and returns the +// ServiceDependency with an id equal to `id`, returns a nil ServiceDependency +// if it is not found. +func (r *resourceServiceDependency) requestGetServiceDependency(ctx context.Context, id, depId, rt string) (*pagerduty.ServiceDependency, diag.Diagnostics) { + var diags diag.Diagnostics + var found *pagerduty.ServiceDependency + + retryErr := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + var list *pagerduty.ListServiceDependencies + var err error + + switch rt { + case "service", "technical_service", "technical_service_reference": + list, err = r.client.ListTechnicalServiceDependenciesWithContext(ctx, depId) + case "business_service", "business_service_reference": + list, err = r.client.ListBusinessServiceDependenciesWithContext(ctx, depId) + default: + err = fmt.Errorf("RT not available: %v", rt) + return retry.RetryableError(err) + } + if err != nil { + // TODO if 400 { + // TODO return retry.NonRetryableError(err) + // TODO } + // Delaying retry by 30s as recommended by PagerDuty + // https://developer.pagerduty.com/docs/rest-api-v2/rate-limiting/#what-are-possible-workarounds-to-the-events-api-rate-limit + time.Sleep(30 * time.Second) + return retry.RetryableError(err) + } + + for _, rel := range list.Relationships { + if rel.ID == id { + found = rel + break + } + } + return nil + }) + if retryErr != nil { + diags.AddError("Error listing service dependencies", retryErr.Error()) + } + return found, diags +} + +func (r *resourceServiceDependency) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceServiceDependency) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + ids := strings.Split(req.ID, ".") + if len(ids) != 3 { + resp.Diagnostics.AddError( + "Error importing pagerduty_service_dependency", + "Expecting an importation ID formed as '..'", + ) + } + supId, supRt, id := ids[0], ids[1], ids[2] + serviceDependency, diags := r.requestGetServiceDependency(ctx, id, supId, supRt) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + model, diags := flattenServiceDependency([]*pagerduty.ServiceDependency{serviceDependency}) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +var supportingServiceObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + }, +} + +var dependentServiceObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + }, +} + +var serviceDependencyObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "supporting_service": types.ListType{ + ElemType: supportingServiceObjectType, + }, + "dependent_service": types.ListType{ + ElemType: supportingServiceObjectType, + }, + }, +} + +type resourceServiceDependencyItemModel struct { + SupportingService types.List `tfsdk:"supporting_service"` + DependentService types.List `tfsdk:"dependent_service"` + Type types.String `tfsdk:"type"` +} + +type resourceServiceDependencyModel struct { + ID types.String `tfsdk:"id"` + Dependency types.List `tfsdk:"dependency"` +} + +var resourceServiceDependencyMu sync.Mutex + +func buildServiceDependencyStruct(ctx context.Context, model resourceServiceDependencyModel) (*pagerduty.ServiceDependency, diag.Diagnostics) { + var diags diag.Diagnostics + + var dependency []*resourceServiceDependencyItemModel + if d := model.Dependency.ElementsAs(ctx, &dependency, false); d.HasError() { + return nil, d + } + + // These branches should not happen because of schema Validation + if len(dependency) < 1 { + diags.AddError("dependency length < 1", "") + return nil, diags + } + if len(dependency[0].SupportingService.Elements()) < 1 { + diags.AddError("supporting service not found for dependency", "") + } + if len(dependency[0].DependentService.Elements()) < 1 { + diags.AddError("dependent service not found for dependency", "") + } + if diags.HasError() { + return nil, diags + } + // ^These branches should not happen because of schema Validation + + ss, d := buildServiceObj(ctx, dependency[0].SupportingService.Elements()[0]) + if d.HasError() { + diags.Append(d...) + return nil, diags + } + ds, d := buildServiceObj(ctx, dependency[0].DependentService.Elements()[0]) + if d.HasError() { + diags.Append(d...) + return nil, diags + } + + serviceDependency := &pagerduty.ServiceDependency{ + ID: model.ID.ValueString(), + Type: dependency[0].Type.ValueString(), + SupportingService: ss, + DependentService: ds, + } + + return serviceDependency, diags +} + +func buildServiceObj(ctx context.Context, model attr.Value) (*pagerduty.ServiceObj, diag.Diagnostics) { + var diags diag.Diagnostics + obj, ok := model.(types.Object) + if !ok { + diags.AddError("Not ok", "") + return nil, diags + } + var serviceRef struct { + ID string `tfsdk:"id"` + Type string `tfsdk:"type"` + } + obj.As(ctx, &serviceRef, basetypes.ObjectAsOptions{}) + serviceObj := pagerduty.ServiceObj(serviceRef) + return &serviceObj, diags +} + +func flattenServiceReference(objType types.ObjectType, src *pagerduty.ServiceObj) (list types.List, diags diag.Diagnostics) { + if src == nil { + diags.AddError("service reference is null", "") + return + } + + serviceRef, d := types.ObjectValue(objType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(src.ID), + "type": types.StringValue(convertServiceDependencyType(src.Type)), + }) + if diags.Append(d...); diags.HasError() { + return + } + + list, d = types.ListValue(supportingServiceObjectType, []attr.Value{serviceRef}) + diags.Append(d...) + return +} + +func flattenServiceDependency(list []*pagerduty.ServiceDependency) (model resourceServiceDependencyModel, diags diag.Diagnostics) { + if len(list) < 1 { + diags.AddError("Pagerduty did not responded with any dependency", "") + return + } + item := list[0] + + supportingService, d := flattenServiceReference(supportingServiceObjectType, item.SupportingService) + if diags.Append(d...); diags.HasError() { + return + } + + dependentService, d := flattenServiceReference(dependentServiceObjectType, item.DependentService) + if diags.Append(d...); diags.HasError() { + return + } + + dependency, d := types.ObjectValue( + serviceDependencyObjectType.AttrTypes, + map[string]attr.Value{ + "type": types.StringValue(item.Type), + "supporting_service": supportingService, + "dependent_service": dependentService, + }, + ) + if diags.Append(d...); diags.HasError() { + return model, diags + } + + model.ID = types.StringValue(item.ID) + dependencyList, d := types.ListValue(serviceDependencyObjectType, []attr.Value{dependency}) + if diags.Append(d...); diags.HasError() { + return model, diags + } + model.Dependency = dependencyList + + return model, diags +} + +// convertServiceDependencyType is needed because the PagerDuty API returns +// '*_reference' values in the response but uses the other kind of values in +// requests +func convertServiceDependencyType(s string) string { + switch s { + case "business_service_reference": + s = "business_service" + case "technical_service_reference": + s = "service" + } + return s +} diff --git a/pagerdutyplugin/resource_pagerduty_service_dependency_test.go b/pagerdutyplugin/resource_pagerduty_service_dependency_test.go new file mode 100644 index 000000000..269154080 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_service_dependency_test.go @@ -0,0 +1,634 @@ +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" +) + +// Testing Business Service Dependencies +func TestAccPagerDutyBusinessServiceDependency_Basic(t *testing.T) { + service := fmt.Sprintf("tf-%s", acctest.RandString(5)) + businessService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + username := fmt.Sprintf("tf-%s", acctest.RandString(5)) + email := fmt.Sprintf("%s@foo.test", username) + escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyBusinessServiceDependencyExists("pagerduty_service_dependency.foo"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.foo", "dependency.#", "1"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.foo", "dependency.0.supporting_service.#", "1"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.foo", "dependency.0.dependent_service.#", "1"), + ), + }, + // Validating that externally removed business service dependencies are + // detected and planned for re-creation + { + Config: testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy), + Check: resource.ComposeTestCheckFunc( + testAccExternallyDestroyServiceDependency("pagerduty_service_dependency.foo", "pagerduty_business_service.foo", "pagerduty_service.foo"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +// Testing Parallel creation of Business Service Dependencies +func TestAccPagerDutyBusinessServiceDependency_Parallel(t *testing.T) { + service := fmt.Sprintf("tf-%s", acctest.RandString(5)) + businessService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + username := fmt.Sprintf("tf-%s", acctest.RandString(5)) + email := fmt.Sprintf("%s@foo.test", username) + escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) + resCount := 30 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyBusinessServiceDependencyParallelConfig(service, businessService, username, email, escalationPolicy, resCount), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyBusinessServiceDependencyParallelExists("pagerduty_service_dependency.foo", resCount), + ), + }, + }, + }) +} + +func testAccCheckPagerDutyBusinessServiceDependencyExists(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 Service Relationship ID is set") + } + businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] + + client := testAccProvider.client + + ctx := context.TODO() + depResp, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) + if err != nil { + return fmt.Errorf("Business Service not found: %v", err) + } + var foundRel *pagerduty.ServiceDependency + + // loop serviceRelationships until relationship.IDs match + for _, rel := range depResp.Relationships { + if rel.ID == rs.Primary.ID { + foundRel = rel + break + } + } + if foundRel == nil { + return fmt.Errorf("Service Dependency not found: %v", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckPagerDutyBusinessServiceDependencyParallelExists(n string, resCount int) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := []*terraform.ResourceState{} + for i := 0; i < resCount; i++ { + resName := fmt.Sprintf("%s.%d", n, i) + r, ok := s.RootModule().Resources[resName] + if !ok { + return fmt.Errorf("Not found: %s", resName) + } + rs = append(rs, r) + } + + for _, r := range rs { + if r.Primary.ID == "" { + return fmt.Errorf("No Service Relationship ID is set") + } + } + + for i := 0; i < resCount; i++ { + businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] + + client := testAccProvider.client + + ctx := context.TODO() + depResp, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) + if err != nil { + return fmt.Errorf("Business Service not found: %v", err) + } + var foundRel *pagerduty.ServiceDependency + + // loop serviceRelationships until relationship.IDs match + for _, rel := range depResp.Relationships { + if rel.ID == rs[i].Primary.ID { + foundRel = rel + break + } + } + if foundRel == nil { + return fmt.Errorf("Service Dependency not found: %v", rs[i].Primary.ID) + } + } + + return nil + } +} + +func testAccCheckPagerDutyBusinessServiceDependencyDestroy(s *terraform.State) error { + client := testAccProvider.client + for _, r := range s.RootModule().Resources { + if r.Type != "pagerduty_service_dependency" { + continue + } + businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] + + // get business service + ctx := context.TODO() + dependencies, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) + if err != nil { + // if the business service doesn't exist, that's okay + return nil + } + // get business service dependencies + for _, rel := range dependencies.Relationships { + if rel.ID == r.Primary.ID { + return fmt.Errorf("supporting service relationship still exists") + } + } + + } + return nil +} + +func testAccCheckPagerDutyBusinessServiceDependencyParallelConfig(service, businessService, username, email, escalationPolicy string, resCount int) string { + return fmt.Sprintf(` +resource "pagerduty_business_service" "foo" { + name = "%[1]s" +} + +resource "pagerduty_user" "foo" { + name = "%[2]s" + email = "%[3]s" + color = "green" + role = "user" + job_title = "foo" + description = "foo" +} + +resource "pagerduty_escalation_policy" "foo" { + name = "%[4]s" + description = "bar" + num_loops = 2 + rule { + escalation_delay_in_minutes = 10 + target { + type = "user_reference" + id = pagerduty_user.foo.id + } + } +} +resource "pagerduty_service" "supportBar" { + count = %[6]d + name = "%[5]s-${count.index}" + description = "foo" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.foo.id + alert_creation = "create_incidents" +} +resource "pagerduty_service_dependency" "foo" { + count = %[6]d + dependency { + dependent_service { + id = pagerduty_business_service.foo.id + type = "business_service" + } + supporting_service { + id = pagerduty_service.supportBar[count.index].id + type = "service" + } + } +} +`, businessService, username, email, escalationPolicy, service, resCount) +} + +func testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy string) string { + return fmt.Sprintf(` +resource "pagerduty_business_service" "foo" { + name = "%s" +} + +resource "pagerduty_user" "foo" { + name = "%s" + email = "%s" + color = "green" + role = "user" + job_title = "foo" + description = "foo" +} + +resource "pagerduty_escalation_policy" "foo" { + name = "%s" + description = "bar" + num_loops = 2 + rule { + escalation_delay_in_minutes = 10 + target { + type = "user_reference" + id = pagerduty_user.foo.id + } + } +} +resource "pagerduty_service" "foo" { + name = "%s" + description = "foo" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.foo.id + alert_creation = "create_incidents" +} +resource "pagerduty_service_dependency" "foo" { + dependency { + dependent_service { + id = pagerduty_business_service.foo.id + type = "business_service" + } + supporting_service { + id = pagerduty_service.foo.id + type = "service" + } + } +} +`, businessService, username, email, escalationPolicy, service) +} + +func testAccExternallyDestroyServiceDependency(resName, depName, suppName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resName] + if !ok { + return fmt.Errorf("Not found: %s", resName) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Service Dependency ID is set for %q", resName) + } + + dep, ok := s.RootModule().Resources[depName] + if !ok { + return fmt.Errorf("Not found: %s", depName) + } + if dep.Primary.ID == "" { + return fmt.Errorf("No Dependent Business Service ID is set for %q", depName) + } + depServiceType := dep.Primary.Attributes["type"] + + supp, ok := s.RootModule().Resources[suppName] + if !ok { + return fmt.Errorf("Not found: %s", suppName) + } + if supp.Primary.ID == "" { + return fmt.Errorf("No Supporting Service ID is set for %q", suppName) + } + suppServiceType := supp.Primary.Attributes["type"] + + client := testAccProvider.client + var r []*pagerduty.ServiceDependency + r = append(r, &pagerduty.ServiceDependency{ + ID: rs.Primary.ID, + DependentService: &pagerduty.ServiceObj{ + ID: dep.Primary.ID, + Type: depServiceType, + }, + SupportingService: &pagerduty.ServiceObj{ + ID: supp.Primary.ID, + Type: suppServiceType, + }, + }) + input := pagerduty.ListServiceDependencies{ + Relationships: r, + } + ctx := context.TODO() + _, err := client.DisassociateServiceDependenciesWithContext(ctx, &input) + if err != nil { + return err + } + + return nil + } +} + +// Testing Technical Service Dependencies +func TestAccPagerDutyTechnicalServiceDependency_Basic(t *testing.T) { + dependentService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + supportingService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + username := fmt.Sprintf("tf-%s", acctest.RandString(5)) + email := fmt.Sprintf("%s@foo.test", username) + escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyDestroy("pagerduty_service.supportBar"), + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyTechnicalServiceDependencyConfig(dependentService, supportingService, username, email, escalationPolicy), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyTechnicalServiceDependencyExists("pagerduty_service_dependency.bar"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.bar", "dependency.#", "1"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.bar", "dependency.0.supporting_service.#", "1"), + resource.TestCheckResourceAttr( + "pagerduty_service_dependency.bar", "dependency.0.dependent_service.#", "1"), + ), + }, + // Validating that externally removed technical service dependencies are + // detected and planned for re-creation + { + Config: testAccCheckPagerDutyTechnicalServiceDependencyConfig(dependentService, supportingService, username, email, escalationPolicy), + Check: resource.ComposeTestCheckFunc( + testAccExternallyDestroyServiceDependency("pagerduty_service_dependency.bar", "pagerduty_service.dependBar", "pagerduty_service.supportBar"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +// Testing Parallel creation of Technical Service Dependencies +func TestAccPagerDutyTechnicalServiceDependency_Parallel(t *testing.T) { + dependentService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + supportingService := fmt.Sprintf("tf-%s", acctest.RandString(5)) + username := fmt.Sprintf("tf-%s", acctest.RandString(5)) + email := fmt.Sprintf("%s@foo.test", username) + escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) + resCount := 30 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyParallelDestroy("pagerduty_service.supportBar", resCount), + Steps: []resource.TestStep{ + { + Config: testAccCheckPagerDutyTechnicalServiceDependencyParallelConfig(dependentService, supportingService, username, email, escalationPolicy, resCount), + Check: resource.ComposeTestCheckFunc( + testAccCheckPagerDutyTechnicalServiceDependencyParallelExists("pagerduty_service_dependency.bar", resCount), + ), + }, + }, + }) +} + +func testAccCheckPagerDutyTechnicalServiceDependencyExists(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 Service Relationship ID is set") + } + supportService, _ := s.RootModule().Resources["pagerduty_service.supportBar"] + + client := testAccProvider.client + + ctx := context.TODO() + depResp, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) + if err != nil { + return fmt.Errorf("Technical Service not found: %v", err) + } + var foundRel *pagerduty.ServiceDependency + + // loop serviceRelationships until relationship.IDs match + for _, rel := range depResp.Relationships { + if rel.ID == rs.Primary.ID { + foundRel = rel + break + } + } + if foundRel == nil { + return fmt.Errorf("Service Dependency not found: %v", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckPagerDutyTechnicalServiceDependencyParallelExists(n string, resCount int) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := []*terraform.ResourceState{} + for i := 0; i < resCount; i++ { + resName := fmt.Sprintf("%s.%d", n, i) + r, ok := s.RootModule().Resources[resName] + if !ok { + return fmt.Errorf("Not found: %s", resName) + } + rs = append(rs, r) + } + + for _, r := range rs { + if r.Primary.ID == "" { + return fmt.Errorf("No Service Relationship ID is set") + } + } + + for i := 0; i < resCount; i++ { + resName := fmt.Sprintf("pagerduty_service.supportBar.%d", i) + supportService, _ := s.RootModule().Resources[resName] + + client := testAccProvider.client + + ctx := context.TODO() + depResp, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) + if err != nil { + return fmt.Errorf("Technical Service not found: %v", err) + } + var foundRel *pagerduty.ServiceDependency + + // loop serviceRelationships until relationship.IDs match + for _, rel := range depResp.Relationships { + if rel.ID == rs[i].Primary.ID { + foundRel = rel + break + } + } + if foundRel == nil { + return fmt.Errorf("Service Dependency not found: %v", rs[i].Primary.ID) + } + } + + return nil + } +} + +func testAccCheckPagerDutyTechnicalServiceDependencyParallelDestroy(n string, resCount int) resource.TestCheckFunc { + return func(s *terraform.State) error { + for i := 0; i < resCount; i++ { + if err := testAccCheckPagerDutyTechnicalServiceDependencyDestroy(fmt.Sprintf("%s.%d", n, i))(s); err != nil { + return err + } + } + return nil + } +} + +func testAccCheckPagerDutyTechnicalServiceDependencyDestroy(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.client + for _, r := range s.RootModule().Resources { + if r.Type != "pagerduty_service_dependency" { + continue + } + supportService, _ := s.RootModule().Resources[n] + + // get service dependencies + ctx := context.TODO() + dependencies, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) + if err != nil { + // if the dependency doesn't exist, that's okay + return nil + } + // find desired dependency + for _, rel := range dependencies.Relationships { + if rel.ID == r.Primary.ID { + return fmt.Errorf("supporting service relationship still exists") + } + } + + } + return nil + } +} + +func testAccCheckPagerDutyTechnicalServiceDependencyConfig(dependentService, supportingService, username, email, escalationPolicy string) string { + return fmt.Sprintf(` + + +resource "pagerduty_user" "bar" { + name = "%s" + email = "%s" + color = "green" + role = "user" + job_title = "foo" + description = "foo" +} + +resource "pagerduty_escalation_policy" "bar" { + name = "%s" + description = "bar-desc" + num_loops = 2 + rule { + escalation_delay_in_minutes = 10 + target { + type = "user_reference" + id = pagerduty_user.bar.id + } + } +} +resource "pagerduty_service" "supportBar" { + name = "%s" + description = "supportBarDesc" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.bar.id + alert_creation = "create_incidents" +} +resource "pagerduty_service" "dependBar" { + name = "%s" + description = "dependBarDesc" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.bar.id + alert_creation = "create_incidents" +} +resource "pagerduty_service_dependency" "bar" { + dependency { + dependent_service { + id = pagerduty_service.dependBar.id + type = "service" + } + supporting_service { + id = pagerduty_service.supportBar.id + type = "service" + } + } +} +`, username, email, escalationPolicy, supportingService, dependentService) +} + +func testAccCheckPagerDutyTechnicalServiceDependencyParallelConfig(dependentService, supportingService, username, email, escalationPolicy string, resCount int) string { + return fmt.Sprintf(` +resource "pagerduty_user" "bar" { + name = "%[1]s" + email = "%[2]s" + color = "green" + role = "user" + job_title = "foo" + description = "foo" +} + +resource "pagerduty_escalation_policy" "bar" { + name = "%[3]s" + description = "bar-desc" + num_loops = 2 + rule { + escalation_delay_in_minutes = 10 + target { + type = "user_reference" + id = pagerduty_user.bar.id + } + } +} +resource "pagerduty_service" "supportBar" { + count = %[6]d + name = "%[4]s-${count.index}" + description = "supportBarDesc" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.bar.id + alert_creation = "create_incidents" +} +resource "pagerduty_service" "dependBar" { + name = "%[5]s" + description = "dependBarDesc" + auto_resolve_timeout = 1800 + acknowledgement_timeout = 1800 + escalation_policy = pagerduty_escalation_policy.bar.id + alert_creation = "create_incidents" +} +resource "pagerduty_service_dependency" "bar" { + count = %[6]d + dependency { + dependent_service { + id = pagerduty_service.dependBar.id + type = "service" + } + supporting_service { + id = pagerduty_service.supportBar[count.index].id + type = "service" + } + } +} +`, username, email, escalationPolicy, supportingService, dependentService, resCount) +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/all.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/all.go new file mode 100644 index 000000000..a0ada2099 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/all.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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.List) validator.List { + return allValidator{ + validators: validators, + } +} + +var _ validator.List = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []validator.List +} + +// 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) +} + +// ValidateList performs the validation. +func (v allValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.ListResponse{} + + subValidator.ValidateList(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/also_requires.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/also_requires.go new file mode 100644 index 000000000..9a666c9e3 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/also_requires.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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 or block 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 or block +// being validated. +func AlsoRequires(expressions ...path.Expression) validator.List { + return schemavalidator.AlsoRequiresValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any.go new file mode 100644 index 000000000..2fbb5f388 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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.List) validator.List { + return anyValidator{ + validators: validators, + } +} + +var _ validator.List = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []validator.List +} + +// 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) +} + +// ValidateList performs the validation. +func (v anyValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.ListResponse{} + + subValidator.ValidateList(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/listvalidator/any_with_all_warnings.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any_with_all_warnings.go new file mode 100644 index 000000000..de9ead9a0 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any_with_all_warnings.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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.List) validator.List { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ validator.List = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []validator.List +} + +// 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) +} + +// ValidateList performs the validation. +func (v anyWithAllWarningsValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &validator.ListResponse{} + + subValidator.ValidateList(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/listvalidator/at_least_one_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/at_least_one_of.go new file mode 100644 index 000000000..2de2fbb07 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/at_least_one_of.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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 or block 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 or block +// being validated. +func AtLeastOneOf(expressions ...path.Expression) validator.List { + return schemavalidator.AtLeastOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/conflicts_with.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/conflicts_with.go new file mode 100644 index 000000000..a8f35d068 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/conflicts_with.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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 or block 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 or block +// being validated. +func ConflictsWith(expressions ...path.Expression) validator.List { + return schemavalidator.ConflictsWithValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/doc.go new file mode 100644 index 000000000..a13b37615 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package listvalidator provides validators for types.List attributes. +package listvalidator diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/exactly_one_of.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/exactly_one_of.go new file mode 100644 index 000000000..25fa59bf3 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/exactly_one_of.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +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 or block 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 or block +// being validated. +func ExactlyOneOf(expressions ...path.Expression) validator.List { + return schemavalidator.ExactlyOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/is_required.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/is_required.go new file mode 100644 index 000000000..c4f8a6f97 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/is_required.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = isRequiredValidator{} + +// isRequiredValidator validates that a list has a configuration value. +type isRequiredValidator struct{} + +// Description describes the validation in plain text formatting. +func (v isRequiredValidator) Description(_ context.Context) string { + return "must have a configuration value as the provider has marked it as required" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v isRequiredValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v isRequiredValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() { + resp.Diagnostics.Append(validatordiag.InvalidBlockDiagnostic( + req.Path, + v.Description(ctx), + )) + } +} + +// IsRequired returns a validator which ensures that any configured list has a value (not null). +// +// This validator is equivalent to the `Required` field on attributes and is only +// practical for use with `schema.ListNestedBlock` +func IsRequired() validator.List { + return isRequiredValidator{} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_least.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_least.go new file mode 100644 index 000000000..bfe35e7d1 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_least.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = sizeAtLeastValidator{} + +// sizeAtLeastValidator validates that list contains at least min elements. +type sizeAtLeastValidator struct { + min int +} + +// Description describes the validation in plain text formatting. +func (v sizeAtLeastValidator) Description(_ context.Context) string { + return fmt.Sprintf("list must contain at least %d elements", v.min) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elems := req.ConfigValue.Elements() + + if len(elems) < v.min { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + } +} + +// SizeAtLeast returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a List. +// - Contains at least min elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func SizeAtLeast(min int) validator.List { + return sizeAtLeastValidator{ + min: min, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_most.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_most.go new file mode 100644 index 000000000..f3e7b36d8 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_most.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = sizeAtMostValidator{} + +// sizeAtMostValidator validates that list contains at most max elements. +type sizeAtMostValidator struct { + max int +} + +// Description describes the validation in plain text formatting. +func (v sizeAtMostValidator) Description(_ context.Context) string { + return fmt.Sprintf("list must contain at most %d elements", v.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elems := req.ConfigValue.Elements() + + if len(elems) > v.max { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + } +} + +// SizeAtMost returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a List. +// - Contains at most max elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func SizeAtMost(max int) validator.List { + return sizeAtMostValidator{ + max: max, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_between.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_between.go new file mode 100644 index 000000000..32c34d9e6 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_between.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = sizeBetweenValidator{} + +// sizeBetweenValidator validates that list contains at least min elements +// and at most max elements. +type sizeBetweenValidator struct { + min int + max int +} + +// Description describes the validation in plain text formatting. +func (v sizeBetweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("list must contain at least %d elements and at most %d elements", v.min, v.max) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// Validate performs the validation. +func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elems := req.ConfigValue.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + )) + } +} + +// SizeBetween returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is a List. +// - Contains at least min elements and at most max elements. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func SizeBetween(min, max int) validator.List { + return sizeBetweenValidator{ + min: min, + max: max, + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/unique_values.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/unique_values.go new file mode 100644 index 000000000..6cfc3b73a --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/unique_values.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = uniqueValuesValidator{} + +// uniqueValuesValidator implements the validator. +type uniqueValuesValidator struct{} + +// Description returns the plaintext description of the validator. +func (v uniqueValuesValidator) Description(_ context.Context) string { + return "all values must be unique" +} + +// MarkdownDescription returns the Markdown description of the validator. +func (v uniqueValuesValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateList implements the validation logic. +func (v uniqueValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elements := req.ConfigValue.Elements() + + for indexOuter, elementOuter := range elements { + // Only evaluate known values for duplicates. + if elementOuter.IsUnknown() { + continue + } + + for indexInner := indexOuter + 1; indexInner < len(elements); indexInner++ { + elementInner := elements[indexInner] + + if elementInner.IsUnknown() { + continue + } + + if !elementInner.Equal(elementOuter) { + continue + } + + resp.Diagnostics.AddAttributeError( + req.Path, + "Duplicate List Value", + fmt.Sprintf("This attribute contains duplicate values of: %s", elementInner), + ) + } + } +} + +// UniqueValues returns a validator which ensures that any configured list +// only contains unique values. This is similar to using a set attribute type +// which inherently validates unique values, but with list ordering semantics. +// Null (unconfigured) and unknown (known after apply) values are skipped. +func UniqueValues() validator.List { + return uniqueValuesValidator{} +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_float64s_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_float64s_are.go new file mode 100644 index 000000000..708e08781 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_float64s_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueFloat64sAre returns an validator which ensures that any configured +// Float64 values passes each Float64 validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueFloat64sAre(elementValidators ...validator.Float64) validator.List { + return valueFloat64sAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueFloat64sAreValidator{} + +// valueFloat64sAreValidator validates that each Float64 member validates against each of the value validators. +type valueFloat64sAreValidator struct { + elementValidators []validator.Float64 +} + +// Description describes the validation in plain text formatting. +func (v valueFloat64sAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueFloat64sAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateFloat64 performs the validation. +func (v valueFloat64sAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.Float64Typable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Float64 values validator, however its values do not implement types.Float64Type or the types.Float64Typable interface for custom Float64 types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.Float64Valuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Float64 values validator, however its values do not implement types.Float64Type or the types.Float64Typable interface for custom Float64 types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToFloat64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.Float64Request{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.Float64Response{} + + elementValidator.ValidateFloat64(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_int64s_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_int64s_are.go new file mode 100644 index 000000000..6cdc0ce05 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_int64s_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueInt64sAre returns an validator which ensures that any configured +// Int64 values passes each Int64 validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueInt64sAre(elementValidators ...validator.Int64) validator.List { + return valueInt64sAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueInt64sAreValidator{} + +// valueInt64sAreValidator validates that each Int64 member validates against each of the value validators. +type valueInt64sAreValidator struct { + elementValidators []validator.Int64 +} + +// Description describes the validation in plain text formatting. +func (v valueInt64sAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueInt64sAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateInt64 performs the validation. +func (v valueInt64sAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.Int64Typable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Int64 values validator, however its values do not implement types.Int64Type or the types.Int64Typable interface for custom Int64 types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.Int64Valuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Int64 values validator, however its values do not implement types.Int64Type or the types.Int64Typable interface for custom Int64 types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToInt64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.Int64Request{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.Int64Response{} + + elementValidator.ValidateInt64(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_lists_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_lists_are.go new file mode 100644 index 000000000..6ebf116d7 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_lists_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueListsAre returns an validator which ensures that any configured +// List values passes each List validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueListsAre(elementValidators ...validator.List) validator.List { + return valueListsAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueListsAreValidator{} + +// valueListsAreValidator validates that each List member validates against each of the value validators. +type valueListsAreValidator struct { + elementValidators []validator.List +} + +// Description describes the validation in plain text formatting. +func (v valueListsAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueListsAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateSet performs the validation. +func (v valueListsAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.ListTypable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a List values validator, however its values do not implement types.ListType or the types.ListTypable interface for custom List types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.ListValuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a List values validator, however its values do not implement types.ListType or the types.ListTypable interface for custom List types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.ListRequest{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.ListResponse{} + + elementValidator.ValidateList(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_maps_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_maps_are.go new file mode 100644 index 000000000..ececd13cc --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_maps_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueMapsAre returns an validator which ensures that any configured +// Map values passes each Map validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueMapsAre(elementValidators ...validator.Map) validator.List { + return valueMapsAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueMapsAreValidator{} + +// valueMapsAreValidator validates that each Map member validates against each of the value validators. +type valueMapsAreValidator struct { + elementValidators []validator.Map +} + +// Description describes the validation in plain text formatting. +func (v valueMapsAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueMapsAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateMap performs the validation. +func (v valueMapsAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.MapTypable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Map values validator, however its values do not implement types.MapType or the types.MapTypable interface for custom Map types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.MapValuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Map values validator, however its values do not implement types.MapType or the types.MapTypable interface for custom Map types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToMapValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.MapRequest{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.MapResponse{} + + elementValidator.ValidateMap(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_numbers_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_numbers_are.go new file mode 100644 index 000000000..7e75e98e1 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_numbers_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueNumbersAre returns an validator which ensures that any configured +// Number values passes each Number validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueNumbersAre(elementValidators ...validator.Number) validator.List { + return valueNumbersAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueNumbersAreValidator{} + +// valueNumbersAreValidator validates that each Number member validates against each of the value validators. +type valueNumbersAreValidator struct { + elementValidators []validator.Number +} + +// Description describes the validation in plain text formatting. +func (v valueNumbersAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueNumbersAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateNumber performs the validation. +func (v valueNumbersAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.NumberTypable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Number values validator, however its values do not implement types.NumberType or the types.NumberTypable interface for custom Number types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.NumberValuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Number values validator, however its values do not implement types.NumberType or the types.NumberTypable interface for custom Number types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToNumberValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.NumberRequest{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.NumberResponse{} + + elementValidator.ValidateNumber(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_sets_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_sets_are.go new file mode 100644 index 000000000..9f05ae117 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_sets_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueSetsAre returns an validator which ensures that any configured +// Set values passes each Set validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueSetsAre(elementValidators ...validator.Set) validator.List { + return valueSetsAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueSetsAreValidator{} + +// valueSetsAreValidator validates that each set member validates against each of the value validators. +type valueSetsAreValidator struct { + elementValidators []validator.Set +} + +// Description describes the validation in plain text formatting. +func (v valueSetsAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueSetsAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateSet performs the validation. +func (v valueSetsAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.SetTypable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Set values validator, however its values do not implement types.SetType or the types.SetTypable interface for custom Set types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.SetValuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a Set values validator, however its values do not implement types.SetType or the types.SetTypable interface for custom Set types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.SetRequest{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.SetResponse{} + + elementValidator.ValidateSet(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_strings_are.go b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_strings_are.go new file mode 100644 index 000000000..ead85b52d --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_strings_are.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueStringsAre returns an validator which ensures that any configured +// String values passes each String validator. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func ValueStringsAre(elementValidators ...validator.String) validator.List { + return valueStringsAreValidator{ + elementValidators: elementValidators, + } +} + +var _ validator.List = valueStringsAreValidator{} + +// valueStringsAreValidator validates that each List member validates against each of the value validators. +type valueStringsAreValidator struct { + elementValidators []validator.String +} + +// Description describes the validation in plain text formatting. +func (v valueStringsAreValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, elementValidator := range v.elementValidators { + descriptions = append(descriptions, elementValidator.Description(ctx)) + } + + return fmt.Sprintf("element value must satisfy all validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v valueStringsAreValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateList performs the validation. +func (v valueStringsAreValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + _, ok := req.ConfigValue.ElementType(ctx).(basetypes.StringTypable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Type", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a String values validator, however its values do not implement types.StringType or the types.StringTypable interface for custom String types. "+ + "Use the appropriate values validator that matches the element type. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)), + ) + + return + } + + for idx, element := range req.ConfigValue.Elements() { + elementPath := req.Path.AtListIndex(idx) + + elementValuable, ok := element.(basetypes.StringValuable) + + // The check above should have prevented this, but raise an error + // instead of a type assertion panic or skipping the element. Any issue + // here likely indicates something wrong in the framework itself. + if !ok { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Validator for Element Value", + "While performing schema-based validation, an unexpected error occurred. "+ + "The attribute declares a String values validator, however its values do not implement types.StringType or the types.StringTypable interface for custom String types. "+ + "This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Path: %s\n", req.Path.String())+ + fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+ + fmt.Sprintf("Element Value Type: %T\n", element), + ) + + return + } + + elementValue, diags := elementValuable.ToStringValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early if the new diagnostics indicate an issue since + // it likely will be the same for all elements. + if diags.HasError() { + return + } + + elementReq := validator.StringRequest{ + Path: elementPath, + PathExpression: elementPath.Expression(), + ConfigValue: elementValue, + Config: req.Config, + } + + for _, elementValidator := range v.elementValidators { + elementResp := &validator.StringResponse{} + + elementValidator.ValidateString(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + } + } +} diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/doc.go new file mode 100644 index 000000000..22fa0a61e --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package listplanmodifier provides plan modifiers for types.List attributes. +package listplanmodifier diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace.go new file mode 100644 index 000000000..eecf57bb4 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listplanmodifier + +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.List { + return RequiresReplaceIf( + func(_ context.Context, _ planmodifier.ListRequest, 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/listplanmodifier/requires_replace_if.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if.go new file mode 100644 index 000000000..840c5223b --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listplanmodifier + +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.List { + 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 +} + +// PlanModifyList implements the plan modification logic. +func (m requiresReplaceIfModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + // 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/listplanmodifier/requires_replace_if_configured.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_configured.go new file mode 100644 index 000000000..81ffdb3d1 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_configured.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listplanmodifier + +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.List { + return RequiresReplaceIf( + func(_ context.Context, req planmodifier.ListRequest, 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/listplanmodifier/requires_replace_if_func.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_func.go new file mode 100644 index 000000000..e6dabd6c2 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_func.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listplanmodifier + +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.ListRequest, *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/listplanmodifier/use_state_for_unknown.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/use_state_for_unknown.go new file mode 100644 index 000000000..c8b2f3bf5 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/use_state_for_unknown.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listplanmodifier + +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.List { + 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." +} + +// PlanModifyList implements the plan modification logic. +func (m useStateForUnknownModifier) PlanModifyList(_ context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + // 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/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go new file mode 100644 index 000000000..6bbbb6607 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package stringplanmodifier provides plan modifiers for types.String attributes. +package stringplanmodifier diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go new file mode 100644 index 000000000..e3adb4b97 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringplanmodifier + +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.String { + return RequiresReplaceIf( + func(_ context.Context, _ planmodifier.StringRequest, 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/stringplanmodifier/requires_replace_if.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go new file mode 100644 index 000000000..0afe6cebf --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringplanmodifier + +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.String { + 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 +} + +// PlanModifyString implements the plan modification logic. +func (m requiresReplaceIfModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // 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/stringplanmodifier/requires_replace_if_configured.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go new file mode 100644 index 000000000..e1bf461dc --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringplanmodifier + +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.String { + return RequiresReplaceIf( + func(_ context.Context, req planmodifier.StringRequest, 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/stringplanmodifier/requires_replace_if_func.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go new file mode 100644 index 000000000..bde13cb3f --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringplanmodifier + +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.StringRequest, *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/stringplanmodifier/use_state_for_unknown.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go new file mode 100644 index 000000000..983bc5cb0 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringplanmodifier + +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.String { + 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." +} + +// PlanModifyString implements the plan modification logic. +func (m useStateForUnknownModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // 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 e6d464d78..414048df9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -165,8 +165,10 @@ 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/defaults +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/tfsdk github.com/hashicorp/terraform-plugin-framework/types @@ -175,6 +177,7 @@ github.com/hashicorp/terraform-plugin-framework/types/basetypes ## explicit; go 1.19 github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag 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 # github.com/hashicorp/terraform-plugin-go v0.20.0 ## explicit; go 1.20