From 421ba33ff49c7e6e4363c5f48f7f5dbf32776271 Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Thu, 1 Feb 2024 10:54:43 -0300 Subject: [PATCH] Add resource pagerduty service dependency --- pagerduty/provider.go | 1 - .../resource_pagerduty_service_dependency.go | 371 ------------- ...mport_pagerduty_service_dependency_test.go | 6 +- pagerdutyplugin/provider.go | 1 + .../resource_pagerduty_service_dependency.go | 515 ++++++++++++++++++ ...ource_pagerduty_service_dependency_test.go | 69 ++- .../listvalidator/all.go | 57 ++ .../listvalidator/also_requires.go | 26 + .../listvalidator/any.go | 65 +++ .../listvalidator/any_with_all_warnings.go | 67 +++ .../listvalidator/at_least_one_of.go | 27 + .../listvalidator/conflicts_with.go | 27 + .../listvalidator/doc.go | 5 + .../listvalidator/exactly_one_of.go | 28 + .../listvalidator/is_required.go | 44 ++ .../listvalidator/size_at_least.go | 59 ++ .../listvalidator/size_at_most.go | 59 ++ .../listvalidator/size_between.go | 62 +++ .../listvalidator/unique_values.go | 68 +++ .../listvalidator/value_float64s_are.go | 119 ++++ .../listvalidator/value_int64s_are.go | 119 ++++ .../listvalidator/value_lists_are.go | 119 ++++ .../listvalidator/value_maps_are.go | 119 ++++ .../listvalidator/value_numbers_are.go | 119 ++++ .../listvalidator/value_sets_are.go | 119 ++++ .../listvalidator/value_strings_are.go | 119 ++++ .../resource/schema/listplanmodifier/doc.go | 5 + .../listplanmodifier/requires_replace.go | 30 + .../listplanmodifier/requires_replace_if.go | 73 +++ .../requires_replace_if_configured.go | 34 ++ .../requires_replace_if_func.go | 25 + .../listplanmodifier/use_state_for_unknown.go | 55 ++ .../resource/schema/stringplanmodifier/doc.go | 5 + .../stringplanmodifier/requires_replace.go | 30 + .../stringplanmodifier/requires_replace_if.go | 73 +++ .../requires_replace_if_configured.go | 34 ++ .../requires_replace_if_func.go | 25 + .../use_state_for_unknown.go | 55 ++ vendor/modules.txt | 3 + 39 files changed, 2435 insertions(+), 402 deletions(-) delete mode 100644 pagerduty/resource_pagerduty_service_dependency.go rename {pagerduty => pagerdutyplugin}/import_pagerduty_service_dependency_test.go (86%) create mode 100644 pagerdutyplugin/resource_pagerduty_service_dependency.go rename {pagerduty => pagerdutyplugin}/resource_pagerduty_service_dependency_test.go (89%) create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/all.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/also_requires.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/any_with_all_warnings.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/at_least_one_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/conflicts_with.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/exactly_one_of.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/is_required.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_least.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_at_most.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/size_between.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/unique_values.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_float64s_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_int64s_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_lists_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_maps_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_numbers_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_sets_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-validators/listvalidator/value_strings_are.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_configured.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/requires_replace_if_func.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier/use_state_for_unknown.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 914f3e800..a0ac22a5c 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -124,7 +124,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/pagerduty/resource_pagerduty_service_dependency.go b/pagerduty/resource_pagerduty_service_dependency.go deleted file mode 100644 index 9a6abbbdf..000000000 --- a/pagerduty/resource_pagerduty_service_dependency.go +++ /dev/null @@ -1,371 +0,0 @@ -package pagerduty - -import ( - "context" - "fmt" - "log" - "net/http" - "strings" - "sync" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/heimweh/go-pagerduty/pagerduty" -) - -// This mutex was introduced in order to avoid race conditions in the backend -// due to parallel calls to `/service_dependencies/associate` endpoint. -var dependencyAssociationMutex sync.Mutex - -func resourcePagerDutyServiceDependency() *schema.Resource { - return &schema.Resource{ - CreateContext: resourcePagerDutyServiceDependencyAssociate, - ReadContext: resourcePagerDutyServiceDependencyRead, - DeleteContext: resourcePagerDutyServiceDependencyDisassociate, - Importer: &schema.ResourceImporter{ - StateContext: resourcePagerDutyServiceDependencyImport, - }, - Schema: map[string]*schema.Schema{ - "dependency": { - Type: schema.TypeList, - Required: true, - ForceNew: true, - MinItems: 1, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "supporting_service": { - Required: true, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validateValueDiagFunc([]string{ - "business_service", - "service", - }), - }, - }, - }, - }, - "dependent_service": { - Required: true, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validateValueDiagFunc([]string{ - "business_service", - "business_service_reference", - "service", - "technical_service_reference", - }), - }, - }, - }, - }, - "type": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - } -} - -func buildServiceDependencyStruct(d *schema.ResourceData) (*pagerduty.ServiceDependency, error) { - rel := new(pagerduty.ServiceDependency) - rel.ID = d.Id() - - for _, r := range d.Get("dependency").([]interface{}) { - relmap := r.(map[string]interface{}) - rel.SupportingService = expandService(relmap["supporting_service"]) - rel.DependentService = expandService(relmap["dependent_service"]) - } - - if rel.SupportingService == nil { - return nil, fmt.Errorf("dependent service not found for dependency: %v", d.Id()) - } - if rel.DependentService == nil { - return nil, fmt.Errorf("supporting service not found for dependency: %v", d.Id()) - } - - if attr, ok := d.GetOk("type"); ok { - rel.Type = attr.(string) - } - return rel, nil -} - -func expandService(v interface{}) *pagerduty.ServiceObj { - var so *pagerduty.ServiceObj - so = new(pagerduty.ServiceObj) - - for _, s := range v.([]interface{}) { - sm := s.(map[string]interface{}) - - so.ID = sm["id"].(string) - so.Type = sm["type"].(string) - } - - return so -} - -func resourcePagerDutyServiceDependencyAssociate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := meta.(*Config).Client() - if err != nil { - return diag.FromErr(err) - } - - serviceDependency, err := buildServiceDependencyStruct(d) - if err != nil { - return diag.FromErr(err) - } - var r []*pagerduty.ServiceDependency - r = append(r, serviceDependency) - - input := pagerduty.ListServiceDependencies{ - Relationships: r, - } - log.Printf("[INFO] Associating PagerDuty dependency %s", serviceDependency.ID) - - var dependencies *pagerduty.ListServiceDependencies - retryErr := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - // Lock the mutex to ensure only one API call to - // `service_dependencies/associate` is done at a time - dependencyAssociationMutex.Lock() - dependencies, _, err = client.ServiceDependencies.AssociateServiceDependencies(&input) - dependencyAssociationMutex.Unlock() - - if err != nil { - if isErrCode(err, 404) { - return retry.RetryableError(err) - } - return retry.NonRetryableError(err) - } else { - for _, r := range dependencies.Relationships { - if err := d.Set("dependency", flattenRelationship(r)); err != nil { - return retry.NonRetryableError(err) - } - d.SetId(r.ID) - } - } - return nil - }) - if retryErr != nil { - time.Sleep(2 * time.Second) - return diag.FromErr(retryErr) - } - return nil -} - -func resourcePagerDutyServiceDependencyDisassociate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := meta.(*Config).Client() - if err != nil { - return diag.FromErr(err) - } - - dependency, err := buildServiceDependencyStruct(d) - if err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] Disassociating PagerDuty dependency %s", dependency.DependentService.ID) - - var foundDep *pagerduty.ServiceDependency - - // listServiceRelationships by calling get dependencies using the serviceDependency.DependentService.ID - retryErr := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - if dependencies, _, err := client.ServiceDependencies.GetServiceDependenciesForType(dependency.DependentService.ID, dependency.DependentService.Type); err != nil { - if isErrCode(err, http.StatusBadRequest) { - return retry.NonRetryableError(err) - } - - // 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) - } else if dependencies != nil { - for _, rel := range dependencies.Relationships { - if rel.ID == d.Id() { - foundDep = rel - break - } - } - } - return nil - }) - if retryErr != nil { - time.Sleep(5 * time.Second) - return diag.FromErr(retryErr) - } - // If the dependency is not found, then chances are it had been deleted - // outside Terraform or be part of a stale state. So it's needed to be cleared - // from the state. - if foundDep == nil { - return nil - } - - // convertType is needed because the PagerDuty API returns the 'reference' values in responses but wants the other - // values in requests - foundDep.SupportingService.Type = convertType(foundDep.SupportingService.Type) - foundDep.DependentService.Type = convertType(foundDep.DependentService.Type) - - // set matching Dependency to r - var r []*pagerduty.ServiceDependency - - r = append(r, foundDep) - - input := pagerduty.ListServiceDependencies{ - Relationships: r, - } - retryErr = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - if _, _, err = client.ServiceDependencies.DisassociateServiceDependencies(&input); err != nil { - if isErrCode(err, http.StatusBadRequest) { - return retry.NonRetryableError(err) - } - - // 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) - } - return nil - }) - if retryErr != nil { - time.Sleep(5 * time.Second) - return diag.FromErr(retryErr) - } - - return nil -} - -func resourcePagerDutyServiceDependencyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - serviceDependency, err := buildServiceDependencyStruct(d) - if err != nil { - return diag.FromErr(err) - } - log.Printf("[INFO] Reading PagerDuty dependency %s", serviceDependency.ID) - - if err = findDependencySetState(ctx, d.Id(), serviceDependency.DependentService.ID, serviceDependency.DependentService.Type, d, meta); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func flattenRelationship(r *pagerduty.ServiceDependency) []map[string]interface{} { - var rels []map[string]interface{} - - relationship := map[string]interface{}{ - "supporting_service": flattenServiceReference(r.SupportingService), - "dependent_service": flattenServiceReference(r.DependentService), - } - rels = append(rels, relationship) - - return rels -} - -func flattenServiceReference(s *pagerduty.ServiceObj) []map[string]interface{} { - var servs []map[string]interface{} - - service := map[string]interface{}{ - "id": s.ID, - "type": convertType(s.Type), - } - servs = append(servs, service) - return servs -} - -// convertType is needed because the PagerDuty API returns the 'reference' values in responses but wants the other -// values in requests -func convertType(s string) string { - switch s { - case "business_service_reference": - s = "business_service" - case "technical_service_reference": - s = "service" - } - return s -} - -func findDependencySetState(ctx context.Context, depID, serviceID, serviceType string, d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - // Pausing to let the PD API sync. - time.Sleep(1 * time.Second) - retryErr := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - if dependencies, _, err := client.ServiceDependencies.GetServiceDependenciesForType(serviceID, serviceType); err != nil { - if isErrCode(err, http.StatusBadRequest) { - return retry.NonRetryableError(err) - } - - // 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) - } else if dependencies != nil { - depFound := false - for _, rel := range dependencies.Relationships { - if rel.ID == depID { - if err := d.Set("dependency", flattenRelationship(rel)); err != nil { - return retry.NonRetryableError(err) - } - d.SetId(rel.ID) - depFound = true - break - } - } - if !depFound { - d.SetId("") - } - } - return nil - }) - if retryErr != nil { - time.Sleep(2 * time.Second) - return retryErr - } - - return nil -} - -func resourcePagerDutyServiceDependencyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - ids := strings.Split(d.Id(), ".") - - if len(ids) != 3 { - return []*schema.ResourceData{}, fmt.Errorf("Error importing pagerduty_service_dependency. Expecting an importation ID formed as '..'") - } - sid, st, id := ids[0], ids[1], ids[2] - - if err := findDependencySetState(ctx, id, sid, st, d, meta); err != nil { - return []*schema.ResourceData{}, err - } - - return []*schema.ResourceData{d}, nil -} diff --git a/pagerduty/import_pagerduty_service_dependency_test.go b/pagerdutyplugin/import_pagerduty_service_dependency_test.go similarity index 86% rename from pagerduty/import_pagerduty_service_dependency_test.go rename to pagerdutyplugin/import_pagerduty_service_dependency_test.go index 35d929ec8..1c5fec4d6 100644 --- a/pagerduty/import_pagerduty_service_dependency_test.go +++ b/pagerdutyplugin/import_pagerduty_service_dependency_test.go @@ -17,9 +17,9 @@ func TestAccPagerDutyServiceDependency_import(t *testing.T) { escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy), 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..f04544b9e --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_service_dependency.go @@ -0,0 +1,515 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + "time" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util" + "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 + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Associating PagerDuty dependency %s", model.ID) + + serviceDependency := buildServiceDependencyStruct(ctx, model, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + dependencies := &pagerduty.ListServiceDependencies{ + Relationships: []*pagerduty.ServiceDependency{serviceDependency}, + } + + var relationships []*pagerduty.ServiceDependency + err := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + resourceServiceDependencyMu.Lock() + list, err := r.client.AssociateServiceDependenciesWithContext(ctx, dependencies) + resourceServiceDependencyMu.Unlock() + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + relationships = list.Relationships + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error associating service dependency %s", model.ID), + err.Error(), + ) + return + } + + model = flattenServiceDependency(relationships, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceServiceDependency) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var model resourceServiceDependencyModel + + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + serviceDependency := buildServiceDependencyStruct(ctx, model, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + log.Printf("Reading PagerDuty dependency %s", serviceDependency.ID) + + serviceDependency, diags := r.requestGetServiceDependency(ctx, serviceDependency.ID, serviceDependency.DependentService.ID, serviceDependency.DependentService.Type) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if serviceDependency == nil { + resp.State.RemoveResource(ctx) + return + } + + model = flattenServiceDependency([]*pagerduty.ServiceDependency{serviceDependency}, &resp.Diagnostics) + if diags.HasError() { + 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) +} + +// 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 := flattenServiceDependency([]*pagerduty.ServiceDependency{serviceDependency}, &resp.Diagnostics) + 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, diags *diag.Diagnostics) *pagerduty.ServiceDependency { + var dependency []*resourceServiceDependencyItemModel + if d := model.Dependency.ElementsAs(ctx, &dependency, false); d.HasError() { + return nil + } + + // These branches should not happen because of schema Validation + if len(dependency) < 1 { + diags.AddError("dependency length < 1", "") + return nil + } + 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 + } + // ^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 + } + ds, d := buildServiceObj(ctx, dependency[0].DependentService.Elements()[0]) + if d.HasError() { + diags.Append(d...) + return nil + } + + serviceDependency := &pagerduty.ServiceDependency{ + ID: model.ID.ValueString(), + Type: dependency[0].Type.ValueString(), + SupportingService: ss, + DependentService: ds, + } + return serviceDependency +} + +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, diags *diag.Diagnostics) (model resourceServiceDependencyModel) { + 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.ID = types.StringValue(item.ID) + dependencyList, d := types.ListValue(serviceDependencyObjectType, []attr.Value{dependency}) + if diags.Append(d...); diags.HasError() { + return + } + model.Dependency = dependencyList + + return +} + +// 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/pagerduty/resource_pagerduty_service_dependency_test.go b/pagerdutyplugin/resource_pagerduty_service_dependency_test.go similarity index 89% rename from pagerduty/resource_pagerduty_service_dependency_test.go rename to pagerdutyplugin/resource_pagerduty_service_dependency_test.go index f31cae7a1..be37afade 100644 --- a/pagerduty/resource_pagerduty_service_dependency_test.go +++ b/pagerdutyplugin/resource_pagerduty_service_dependency_test.go @@ -1,13 +1,14 @@ 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" - "github.com/heimweh/go-pagerduty/pagerduty" ) // Testing Business Service Dependencies @@ -19,9 +20,9 @@ func TestAccPagerDutyBusinessServiceDependency_Basic(t *testing.T) { escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy), @@ -58,9 +59,9 @@ func TestAccPagerDutyBusinessServiceDependency_Parallel(t *testing.T) { resCount := 30 resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyBusinessServiceDependencyDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyBusinessServiceDependencyParallelConfig(service, businessService, username, email, escalationPolicy, resCount), @@ -71,6 +72,7 @@ func TestAccPagerDutyBusinessServiceDependency_Parallel(t *testing.T) { }, }) } + func testAccCheckPagerDutyBusinessServiceDependencyExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -83,9 +85,10 @@ func testAccCheckPagerDutyBusinessServiceDependencyExists(n string) resource.Tes } businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client - depResp, _, err := client.ServiceDependencies.GetServiceDependenciesForType(businessService.Primary.ID, "business_service") + ctx := context.Background() + depResp, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) if err != nil { return fmt.Errorf("Business Service not found: %v", err) } @@ -105,6 +108,7 @@ func testAccCheckPagerDutyBusinessServiceDependencyExists(n string) resource.Tes return nil } } + func testAccCheckPagerDutyBusinessServiceDependencyParallelExists(n string, resCount int) resource.TestCheckFunc { return func(s *terraform.State) error { rs := []*terraform.ResourceState{} @@ -126,9 +130,10 @@ func testAccCheckPagerDutyBusinessServiceDependencyParallelExists(n string, resC for i := 0; i < resCount; i++ { businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client - depResp, _, err := client.ServiceDependencies.GetServiceDependenciesForType(businessService.Primary.ID, "business_service") + ctx := context.Background() + depResp, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) if err != nil { return fmt.Errorf("Business Service not found: %v", err) } @@ -151,7 +156,7 @@ func testAccCheckPagerDutyBusinessServiceDependencyParallelExists(n string, resC } func testAccCheckPagerDutyBusinessServiceDependencyDestroy(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client for _, r := range s.RootModule().Resources { if r.Type != "pagerduty_service_dependency" { continue @@ -159,7 +164,8 @@ func testAccCheckPagerDutyBusinessServiceDependencyDestroy(s *terraform.State) e businessService, _ := s.RootModule().Resources["pagerduty_business_service.foo"] // get business service - dependencies, _, err := client.ServiceDependencies.GetServiceDependenciesForType(businessService.Primary.ID, "business_service") + ctx := context.Background() + dependencies, err := client.ListBusinessServiceDependenciesWithContext(ctx, businessService.Primary.ID) if err != nil { // if the business service doesn't exist, that's okay return nil @@ -174,6 +180,7 @@ func testAccCheckPagerDutyBusinessServiceDependencyDestroy(s *terraform.State) e } return nil } + func testAccCheckPagerDutyBusinessServiceDependencyParallelConfig(service, businessService, username, email, escalationPolicy string, resCount int) string { return fmt.Sprintf(` resource "pagerduty_business_service" "foo" { @@ -225,6 +232,7 @@ resource "pagerduty_service_dependency" "foo" { } `, businessService, username, email, escalationPolicy, service, resCount) } + func testAccCheckPagerDutyBusinessServiceDependencyConfig(service, businessService, username, email, escalationPolicy string) string { return fmt.Sprintf(` resource "pagerduty_business_service" "foo" { @@ -274,6 +282,7 @@ resource "pagerduty_service_dependency" "foo" { } `, 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] @@ -302,7 +311,7 @@ func testAccExternallyDestroyServiceDependency(resName, depName, suppName string } suppServiceType := supp.Primary.Attributes["type"] - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client var r []*pagerduty.ServiceDependency r = append(r, &pagerduty.ServiceDependency{ ID: rs.Primary.ID, @@ -318,7 +327,8 @@ func testAccExternallyDestroyServiceDependency(resName, depName, suppName string input := pagerduty.ListServiceDependencies{ Relationships: r, } - _, _, err := client.ServiceDependencies.DisassociateServiceDependencies(&input) + ctx := context.Background() + _, err := client.DisassociateServiceDependenciesWithContext(ctx, &input) if err != nil { return err } @@ -336,9 +346,9 @@ func TestAccPagerDutyTechnicalServiceDependency_Basic(t *testing.T) { escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyDestroy("pagerduty_service.supportBar"), + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyDestroy("pagerduty_service.supportBar"), Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTechnicalServiceDependencyConfig(dependentService, supportingService, username, email, escalationPolicy), @@ -375,9 +385,9 @@ func TestAccPagerDutyTechnicalServiceDependency_Parallel(t *testing.T) { resCount := 30 resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyParallelDestroy("pagerduty_service.supportBar", resCount), + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTechnicalServiceDependencyParallelDestroy("pagerduty_service.supportBar", resCount), Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTechnicalServiceDependencyParallelConfig(dependentService, supportingService, username, email, escalationPolicy, resCount), @@ -388,6 +398,7 @@ func TestAccPagerDutyTechnicalServiceDependency_Parallel(t *testing.T) { }, }) } + func testAccCheckPagerDutyTechnicalServiceDependencyExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -400,9 +411,10 @@ func testAccCheckPagerDutyTechnicalServiceDependencyExists(n string) resource.Te } supportService, _ := s.RootModule().Resources["pagerduty_service.supportBar"] - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client - depResp, _, err := client.ServiceDependencies.GetServiceDependenciesForType(supportService.Primary.ID, "service") + ctx := context.Background() + depResp, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) if err != nil { return fmt.Errorf("Technical Service not found: %v", err) } @@ -445,9 +457,10 @@ func testAccCheckPagerDutyTechnicalServiceDependencyParallelExists(n string, res resName := fmt.Sprintf("pagerduty_service.supportBar.%d", i) supportService, _ := s.RootModule().Resources[resName] - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client - depResp, _, err := client.ServiceDependencies.GetServiceDependenciesForType(supportService.Primary.ID, "service") + ctx := context.Background() + depResp, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) if err != nil { return fmt.Errorf("Technical Service not found: %v", err) } @@ -482,7 +495,7 @@ func testAccCheckPagerDutyTechnicalServiceDependencyParallelDestroy(n string, re func testAccCheckPagerDutyTechnicalServiceDependencyDestroy(n string) resource.TestCheckFunc { return func(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() + client := testAccProvider.client for _, r := range s.RootModule().Resources { if r.Type != "pagerduty_service_dependency" { continue @@ -490,7 +503,8 @@ func testAccCheckPagerDutyTechnicalServiceDependencyDestroy(n string) resource.T supportService, _ := s.RootModule().Resources[n] // get service dependencies - dependencies, _, err := client.ServiceDependencies.GetServiceDependenciesForType(supportService.Primary.ID, "service") + ctx := context.Background() + dependencies, err := client.ListTechnicalServiceDependenciesWithContext(ctx, supportService.Primary.ID) if err != nil { // if the dependency doesn't exist, that's okay return nil @@ -506,6 +520,7 @@ func testAccCheckPagerDutyTechnicalServiceDependencyDestroy(n string) resource.T return nil } } + func testAccCheckPagerDutyTechnicalServiceDependencyConfig(dependentService, supportingService, username, email, escalationPolicy string) string { return fmt.Sprintf(` 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 c5341e271..e8c53c4f6 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