From 53817f5cfb503e2dfc2535d581d4ba3b8dda99ad Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Thu, 28 Mar 2024 09:17:29 -0300
Subject: [PATCH 01/10] Add use state to resource business service id

---
 .../resource_pagerduty_business_service.go    | 19 +++--
 .../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                            |  1 +
 8 files changed, 235 insertions(+), 7 deletions(-)
 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/pagerdutyplugin/resource_pagerduty_business_service.go b/pagerdutyplugin/resource_pagerduty_business_service.go
index a5482e511..e322f2527 100644
--- a/pagerdutyplugin/resource_pagerduty_business_service.go
+++ b/pagerdutyplugin/resource_pagerduty_business_service.go
@@ -12,7 +12,9 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/path"
 	"github.com/hashicorp/terraform-plugin-framework/resource"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	helperResource "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
@@ -34,11 +36,16 @@ func (r *resourceBusinessService) Metadata(ctx context.Context, req resource.Met
 func (r *resourceBusinessService) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
 	resp.Schema = schema.Schema{
 		Attributes: map[string]schema.Attribute{
-			"name":     schema.StringAttribute{Required: true},
-			"id":       schema.StringAttribute{Computed: true},
-			"html_url": schema.StringAttribute{Computed: true},
-			"self":     schema.StringAttribute{Computed: true},
-			"summary":  schema.StringAttribute{Computed: true},
+			"html_url":         schema.StringAttribute{Computed: true},
+			"name":             schema.StringAttribute{Required: true},
+			"point_of_contact": schema.StringAttribute{Optional: true},
+			"self":             schema.StringAttribute{Computed: true},
+			"summary":          schema.StringAttribute{Computed: true},
+			"team":             schema.StringAttribute{Optional: true},
+			"id": schema.StringAttribute{
+				Computed:      true,
+				PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+			},
 			"description": schema.StringAttribute{
 				Optional: true,
 				Computed: true,
@@ -51,8 +58,6 @@ func (r *resourceBusinessService) Schema(_ context.Context, _ resource.SchemaReq
 				DeprecationMessage: "This will become a computed attribute in the next major release.",
 				Validators:         []validator.String{stringvalidator.OneOf("business_service")},
 			},
-			"point_of_contact": schema.StringAttribute{Optional: true},
-			"team":             schema.StringAttribute{Optional: true},
 		},
 	}
 }
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go
new file mode 100644
index 000000000..6bbbb6607
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/doc.go
@@ -0,0 +1,5 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+// Package stringplanmodifier provides plan modifiers for types.String attributes.
+package stringplanmodifier
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go
new file mode 100644
index 000000000..e3adb4b97
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace.go
@@ -0,0 +1,30 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package stringplanmodifier
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+)
+
+// RequiresReplace returns a plan modifier that conditionally requires
+// resource replacement if:
+//
+//   - The resource is planned for update.
+//   - The plan and state values are not equal.
+//
+// Use RequiresReplaceIfConfigured if the resource replacement should
+// only occur if there is a configuration value (ignore unconfigured drift
+// detection changes). Use RequiresReplaceIf if the resource replacement
+// should check provider-defined conditional logic.
+func RequiresReplace() planmodifier.String {
+	return RequiresReplaceIf(
+		func(_ context.Context, _ planmodifier.StringRequest, resp *RequiresReplaceIfFuncResponse) {
+			resp.RequiresReplace = true
+		},
+		"If the value of this attribute changes, Terraform will destroy and recreate the resource.",
+		"If the value of this attribute changes, Terraform will destroy and recreate the resource.",
+	)
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go
new file mode 100644
index 000000000..0afe6cebf
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if.go
@@ -0,0 +1,73 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package stringplanmodifier
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+)
+
+// RequiresReplaceIf returns a plan modifier that conditionally requires
+// resource replacement if:
+//
+//   - The resource is planned for update.
+//   - The plan and state values are not equal.
+//   - The given function returns true. Returning false will not unset any
+//     prior resource replacement.
+//
+// Use RequiresReplace if the resource replacement should always occur on value
+// changes. Use RequiresReplaceIfConfigured if the resource replacement should
+// occur on value changes, but only if there is a configuration value (ignore
+// unconfigured drift detection changes).
+func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) planmodifier.String {
+	return requiresReplaceIfModifier{
+		ifFunc:              f,
+		description:         description,
+		markdownDescription: markdownDescription,
+	}
+}
+
+// requiresReplaceIfModifier is an plan modifier that sets RequiresReplace
+// on the attribute if a given function is true.
+type requiresReplaceIfModifier struct {
+	ifFunc              RequiresReplaceIfFunc
+	description         string
+	markdownDescription string
+}
+
+// Description returns a human-readable description of the plan modifier.
+func (m requiresReplaceIfModifier) Description(_ context.Context) string {
+	return m.description
+}
+
+// MarkdownDescription returns a markdown description of the plan modifier.
+func (m requiresReplaceIfModifier) MarkdownDescription(_ context.Context) string {
+	return m.markdownDescription
+}
+
+// PlanModifyString implements the plan modification logic.
+func (m requiresReplaceIfModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
+	// Do not replace on resource creation.
+	if req.State.Raw.IsNull() {
+		return
+	}
+
+	// Do not replace on resource destroy.
+	if req.Plan.Raw.IsNull() {
+		return
+	}
+
+	// Do not replace if the plan and state values are equal.
+	if req.PlanValue.Equal(req.StateValue) {
+		return
+	}
+
+	ifFuncResp := &RequiresReplaceIfFuncResponse{}
+
+	m.ifFunc(ctx, req, ifFuncResp)
+
+	resp.Diagnostics.Append(ifFuncResp.Diagnostics...)
+	resp.RequiresReplace = ifFuncResp.RequiresReplace
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go
new file mode 100644
index 000000000..e1bf461dc
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_configured.go
@@ -0,0 +1,34 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package stringplanmodifier
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+)
+
+// RequiresReplaceIfConfigured returns a plan modifier that conditionally requires
+// resource replacement if:
+//
+//   - The resource is planned for update.
+//   - The plan and state values are not equal.
+//   - The configuration value is not null.
+//
+// Use RequiresReplace if the resource replacement should occur regardless of
+// the presence of a configuration value. Use RequiresReplaceIf if the resource
+// replacement should check provider-defined conditional logic.
+func RequiresReplaceIfConfigured() planmodifier.String {
+	return RequiresReplaceIf(
+		func(_ context.Context, req planmodifier.StringRequest, resp *RequiresReplaceIfFuncResponse) {
+			if req.ConfigValue.IsNull() {
+				return
+			}
+
+			resp.RequiresReplace = true
+		},
+		"If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.",
+		"If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.",
+	)
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go
new file mode 100644
index 000000000..bde13cb3f
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/requires_replace_if_func.go
@@ -0,0 +1,25 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package stringplanmodifier
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+)
+
+// RequiresReplaceIfFunc is a conditional function used in the RequiresReplaceIf
+// plan modifier to determine whether the attribute requires replacement.
+type RequiresReplaceIfFunc func(context.Context, planmodifier.StringRequest, *RequiresReplaceIfFuncResponse)
+
+// RequiresReplaceIfFuncResponse is the response type for a RequiresReplaceIfFunc.
+type RequiresReplaceIfFuncResponse struct {
+	// Diagnostics report errors or warnings related to this logic. An empty
+	// or unset slice indicates success, with no warnings or errors generated.
+	Diagnostics diag.Diagnostics
+
+	// RequiresReplace should be enabled if the resource should be replaced.
+	RequiresReplace bool
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go
new file mode 100644
index 000000000..983bc5cb0
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier/use_state_for_unknown.go
@@ -0,0 +1,55 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package stringplanmodifier
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+)
+
+// UseStateForUnknown returns a plan modifier that copies a known prior state
+// value into the planned value. Use this when it is known that an unconfigured
+// value will remain the same after a resource update.
+//
+// To prevent Terraform errors, the framework automatically sets unconfigured
+// and Computed attributes to an unknown value "(known after apply)" on update.
+// Using this plan modifier will instead display the prior state value in the
+// plan, unless a prior plan modifier adjusts the value.
+func UseStateForUnknown() planmodifier.String {
+	return useStateForUnknownModifier{}
+}
+
+// useStateForUnknownModifier implements the plan modifier.
+type useStateForUnknownModifier struct{}
+
+// Description returns a human-readable description of the plan modifier.
+func (m useStateForUnknownModifier) Description(_ context.Context) string {
+	return "Once set, the value of this attribute in state will not change."
+}
+
+// MarkdownDescription returns a markdown description of the plan modifier.
+func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) string {
+	return "Once set, the value of this attribute in state will not change."
+}
+
+// PlanModifyString implements the plan modification logic.
+func (m useStateForUnknownModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
+	// Do nothing if there is no state value.
+	if req.StateValue.IsNull() {
+		return
+	}
+
+	// Do nothing if there is a known planned value.
+	if !req.PlanValue.IsUnknown() {
+		return
+	}
+
+	// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
+	if req.ConfigValue.IsUnknown() {
+		return
+	}
+
+	resp.PlanValue = req.StateValue
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index e6d464d78..2a3ca2012 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -167,6 +167,7 @@ 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/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

From 70b6bc18efba69ad8804bbb297f5fdb8353046bb Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Thu, 28 Mar 2024 08:42:57 -0300
Subject: [PATCH 02/10] Add util.TimeNowInLoc and testAccTimeNow

---
 pagerdutyplugin/provider_test.go | 17 +++++++++++++++--
 util/util.go                     | 13 +++++++++++++
 2 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/pagerdutyplugin/provider_test.go b/pagerdutyplugin/provider_test.go
index 66e63338a..23a1eab5e 100644
--- a/pagerdutyplugin/provider_test.go
+++ b/pagerdutyplugin/provider_test.go
@@ -4,14 +4,15 @@ import (
 	"context"
 	"os"
 	"testing"
+	"time"
 
+	pd "github.com/PagerDuty/terraform-provider-pagerduty/pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
 	"github.com/hashicorp/terraform-plugin-framework/providerserver"
 	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
 	"github.com/hashicorp/terraform-plugin-mux/tf5muxserver"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
-
-	pd "github.com/PagerDuty/terraform-provider-pagerduty/pagerduty"
 )
 
 var testAccProvider = New()
@@ -69,3 +70,15 @@ func testAccProtoV5ProviderFactories() map[string]func() (tfprotov5.ProviderServ
 		},
 	}
 }
+
+// testAccTimeNow returns the current time in the given location.
+// The location defaults to Europe/Dublin but can be controlled by the
+// PAGERDUTY_TIME_ZONE environment variable. The location must match the
+// PagerDuty account time zone or diff issues might bubble up in tests.
+func testAccTimeNow() time.Time {
+	name := "Europe/Dublin"
+	if v := os.Getenv("PAGERDUTY_TIME_ZONE"); v != "" {
+		name = v
+	}
+	return util.TimeNowInLoc(name)
+}
diff --git a/util/util.go b/util/util.go
index 4f24958ba..ca6013509 100644
--- a/util/util.go
+++ b/util/util.go
@@ -26,6 +26,19 @@ func TimeToUTC(v string) (time.Time, error) {
 	return t.UTC(), nil
 }
 
+// TimeNowInLoc returns the current time in the given location.
+// If an error occurs when trying to load the location, we just return the
+// current local time.
+func TimeNowInLoc(name string) time.Time {
+	loc, err := time.LoadLocation(name)
+	now := time.Now()
+	if err != nil {
+		log.Printf("[WARN] Failed to load location: %s", err)
+		return now
+	}
+	return now.In(loc)
+}
+
 // ValidateRFC3339 validates that a date string has the correct RFC3339 layout
 func ValidateRFC3339(v interface{}, k string) (we []string, errors []error) {
 	value := v.(string)

From 87bee283d390d5c34361cd7b18eb509a3b7358ca Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Tue, 6 Feb 2024 11:29:51 -0300
Subject: [PATCH 03/10] Migrate datasource and resource `pagerduty_tag`

---
 pagerduty/data_source_pagerduty_tag.go        |  75 --------
 pagerduty/provider.go                         |   3 +-
 pagerduty/resource_pagerduty_tag.go           | 142 ---------------
 pagerdutyplugin/data_source_pagerduty_tag.go  |  94 ++++++++++
 .../data_source_pagerduty_tag_test.go         |   5 +-
 .../import_pagerduty_tag_test.go              |   6 +-
 pagerdutyplugin/provider.go                   |   2 +
 pagerdutyplugin/resource_pagerduty_tag.go     | 167 ++++++++++++++++++
 .../resource_pagerduty_tag_test.go            |  42 +++--
 9 files changed, 289 insertions(+), 247 deletions(-)
 delete mode 100644 pagerduty/data_source_pagerduty_tag.go
 delete mode 100644 pagerduty/resource_pagerduty_tag.go
 create mode 100644 pagerdutyplugin/data_source_pagerduty_tag.go
 rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_tag_test.go (92%)
 rename {pagerduty => pagerdutyplugin}/import_pagerduty_tag_test.go (74%)
 create mode 100644 pagerdutyplugin/resource_pagerduty_tag.go
 rename {pagerduty => pagerdutyplugin}/resource_pagerduty_tag_test.go (75%)

diff --git a/pagerduty/data_source_pagerduty_tag.go b/pagerduty/data_source_pagerduty_tag.go
deleted file mode 100644
index 4ceb81c26..000000000
--- a/pagerduty/data_source_pagerduty_tag.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package pagerduty
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-func dataSourcePagerDutyTag() *schema.Resource {
-	return &schema.Resource{
-		Read: dataSourcePagerDutyTagRead,
-
-		Schema: map[string]*schema.Schema{
-			"label": {
-				Type:        schema.TypeString,
-				Required:    true,
-				Description: "The label of the tag to find in the PagerDuty API",
-			},
-		},
-	}
-}
-
-func dataSourcePagerDutyTagRead(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Reading PagerDuty tag")
-
-	searchTag := d.Get("label").(string)
-
-	o := &pagerduty.ListTagsOptions{
-		Query: searchTag,
-	}
-
-	return retry.Retry(5*time.Minute, func() *retry.RetryError {
-		resp, _, err := client.Tags.List(o)
-		if 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)
-		}
-
-		var found *pagerduty.Tag
-
-		for _, tag := range resp.Tags {
-			if tag.Label == searchTag {
-				found = tag
-				break
-			}
-		}
-
-		if found == nil {
-			return retry.NonRetryableError(
-				fmt.Errorf("Unable to locate any tag with label: %s", searchTag),
-			)
-		}
-
-		d.SetId(found.ID)
-		d.Set("label", found.Label)
-
-		return nil
-	})
-}
diff --git a/pagerduty/provider.go b/pagerduty/provider.go
index fd6d4ccb0..92b256e86 100644
--- a/pagerduty/provider.go
+++ b/pagerduty/provider.go
@@ -95,7 +95,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_business_service":                           dataSourcePagerDutyBusinessService(),
 			"pagerduty_priority":                                   dataSourcePagerDutyPriority(),
 			"pagerduty_ruleset":                                    dataSourcePagerDutyRuleset(),
-			"pagerduty_tag":                                        dataSourcePagerDutyTag(),
 			"pagerduty_event_orchestration":                        dataSourcePagerDutyEventOrchestration(),
 			"pagerduty_event_orchestrations":                       dataSourcePagerDutyEventOrchestrations(),
 			"pagerduty_event_orchestration_integration":            dataSourcePagerDutyEventOrchestrationIntegration(),
@@ -128,7 +127,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_business_service":                              resourcePagerDutyBusinessService(),
 			"pagerduty_service_dependency":                            resourcePagerDutyServiceDependency(),
 			"pagerduty_response_play":                                 resourcePagerDutyResponsePlay(),
-			"pagerduty_tag":                                           resourcePagerDutyTag(),
 			"pagerduty_tag_assignment":                                resourcePagerDutyTagAssignment(),
 			"pagerduty_service_event_rule":                            resourcePagerDutyServiceEventRule(),
 			"pagerduty_slack_connection":                              resourcePagerDutySlackConnection(),
@@ -156,6 +154,7 @@ func Provider(isMux bool) *schema.Provider {
 
 	if isMux {
 		delete(p.DataSourcesMap, "pagerduty_business_service")
+
 		delete(p.ResourcesMap, "pagerduty_business_service")
 	}
 
diff --git a/pagerduty/resource_pagerduty_tag.go b/pagerduty/resource_pagerduty_tag.go
deleted file mode 100644
index dbe1e4c5e..000000000
--- a/pagerduty/resource_pagerduty_tag.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package pagerduty
-
-import (
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-func resourcePagerDutyTag() *schema.Resource {
-	return &schema.Resource{
-		Create: resourcePagerDutyTagCreate,
-		Read:   resourcePagerDutyTagRead,
-		Delete: resourcePagerDutyTagDelete,
-		Importer: &schema.ResourceImporter{
-			State: schema.ImportStatePassthrough,
-		},
-		Schema: map[string]*schema.Schema{
-			"label": {
-				Type:     schema.TypeString,
-				Required: true,
-				ForceNew: true,
-			},
-			"summary": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-			"html_url": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-		},
-	}
-}
-
-func buildTagStruct(d *schema.ResourceData) *pagerduty.Tag {
-	tag := &pagerduty.Tag{
-		Label: d.Get("label").(string),
-		Type:  "tag",
-	}
-
-	if attr, ok := d.GetOk("summary"); ok {
-		tag.Summary = attr.(string)
-	}
-
-	return tag
-}
-
-func resourcePagerDutyTagCreate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	tag := buildTagStruct(d)
-
-	log.Printf("[INFO] Creating PagerDuty tag %s", tag.Label)
-
-	retryErr := retry.Retry(2*time.Minute, func() *retry.RetryError {
-		if tag, _, err := client.Tags.Create(tag); err != nil {
-			if isErrCode(err, 400) || isErrCode(err, 429) {
-				return retry.RetryableError(err)
-			}
-
-			return retry.NonRetryableError(err)
-		} else if tag != nil {
-			d.SetId(tag.ID)
-		}
-		return nil
-	})
-
-	if retryErr != nil {
-		return retryErr
-	}
-
-	return resourcePagerDutyTagRead(d, meta)
-}
-
-func resourcePagerDutyTagRead(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Reading PagerDuty tag %s", d.Id())
-
-	return retry.Retry(2*time.Minute, func() *retry.RetryError {
-		tag, _, err := client.Tags.Get(d.Id())
-		if err != nil {
-			if isErrCode(err, http.StatusBadRequest) {
-				return retry.NonRetryableError(err)
-			}
-
-			errResp := handleNotFoundError(err, d)
-			if errResp != nil {
-				time.Sleep(2 * time.Second)
-				return retry.RetryableError(errResp)
-			}
-
-			return nil
-		}
-		if tag != nil {
-			log.Printf("Tag Type: %v", tag.Type)
-			d.Set("label", tag.Label)
-			d.Set("summary", tag.Summary)
-			d.Set("html_url", tag.HTMLURL)
-		}
-		return nil
-	})
-}
-
-func resourcePagerDutyTagDelete(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Deleting PagerDuty tag %s", d.Id())
-
-	retryErr := retry.Retry(2*time.Minute, func() *retry.RetryError {
-		if _, err := client.Tags.Delete(d.Id()); err != nil {
-			if isErrCode(err, http.StatusBadRequest) {
-				return retry.NonRetryableError(err)
-			}
-
-			return retry.RetryableError(err)
-		}
-		return nil
-	})
-	if retryErr != nil {
-		time.Sleep(2 * time.Second)
-		return retryErr
-	}
-	d.SetId("")
-
-	// giving the API time to catchup
-	time.Sleep(time.Second)
-	return nil
-}
diff --git a/pagerdutyplugin/data_source_pagerduty_tag.go b/pagerdutyplugin/data_source_pagerduty_tag.go
new file mode 100644
index 000000000..9d0e4d173
--- /dev/null
+++ b/pagerdutyplugin/data_source_pagerduty_tag.go
@@ -0,0 +1,94 @@
+package pagerduty
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+)
+
+type dataSourceTag struct {
+	client *pagerduty.Client
+}
+
+var _ datasource.DataSourceWithConfigure = (*dataSourceStandards)(nil)
+
+func (d *dataSourceTag) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...)
+}
+
+func (d *dataSourceTag) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = "pagerduty_tag"
+}
+
+func (d *dataSourceTag) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"label": schema.StringAttribute{
+				Required:    true,
+				Description: "The label of the tag to find in the PagerDuty API",
+			},
+			"id": schema.StringAttribute{Computed: true},
+		},
+	}
+}
+
+func (d *dataSourceTag) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var searchTag string
+	if d := req.Config.GetAttribute(ctx, path.Root("label"), &searchTag); d.HasError() {
+		resp.Diagnostics.Append(d...)
+		return
+	}
+
+	log.Printf("[INFO] Reading PagerDuty tag")
+
+	var tags []*pagerduty.Tag
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		list, err := d.client.ListTags(pagerduty.ListTagOptions{Query: searchTag})
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		tags = list.Tags
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError("Error reading list of tags", err.Error())
+	}
+
+	var found *pagerduty.Tag
+	for _, tag := range tags {
+		if tag.Label == searchTag {
+			found = tag
+			break
+		}
+	}
+	if found == nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Unable to locate any tag with label: %s", searchTag),
+			"",
+		)
+		return
+	}
+
+	model := dataSourceTagModel{
+		ID:    types.StringValue(found.ID),
+		Label: types.StringValue(found.Label),
+	}
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+type dataSourceTagModel struct {
+	ID    types.String `tfsdk:"id"`
+	Label types.String `tfsdk:"label"`
+}
diff --git a/pagerduty/data_source_pagerduty_tag_test.go b/pagerdutyplugin/data_source_pagerduty_tag_test.go
similarity index 92%
rename from pagerduty/data_source_pagerduty_tag_test.go
rename to pagerdutyplugin/data_source_pagerduty_tag_test.go
index 667799a40..5a3cf4975 100644
--- a/pagerduty/data_source_pagerduty_tag_test.go
+++ b/pagerdutyplugin/data_source_pagerduty_tag_test.go
@@ -13,8 +13,8 @@ func TestAccDataSourcePagerDutyTag_Basic(t *testing.T) {
 	tag := fmt.Sprintf("tf-%s", acctest.RandString(5))
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:  func() { testAccPreCheck(t) },
-		Providers: testAccProviders,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
 		Steps: []resource.TestStep{
 			{
 				Config: testAccDataSourcePagerDutyTagConfig(tag),
@@ -28,7 +28,6 @@ func TestAccDataSourcePagerDutyTag_Basic(t *testing.T) {
 
 func testAccDataSourcePagerDutyTag(src, n string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
-
 		srcR := s.RootModule().Resources[src]
 		srcA := srcR.Primary.Attributes
 
diff --git a/pagerduty/import_pagerduty_tag_test.go b/pagerdutyplugin/import_pagerduty_tag_test.go
similarity index 74%
rename from pagerduty/import_pagerduty_tag_test.go
rename to pagerdutyplugin/import_pagerduty_tag_test.go
index e6e2ae815..b70648b52 100644
--- a/pagerduty/import_pagerduty_tag_test.go
+++ b/pagerdutyplugin/import_pagerduty_tag_test.go
@@ -12,9 +12,9 @@ func TestAccPagerDutyTag_import(t *testing.T) {
 	tag := fmt.Sprintf("tf-%s", acctest.RandString(5))
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagConfig(tag),
diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go
index f7fef54e2..3b55a9349 100644
--- a/pagerdutyplugin/provider.go
+++ b/pagerdutyplugin/provider.go
@@ -53,12 +53,14 @@ func (p *Provider) DataSources(ctx context.Context) [](func() datasource.DataSou
 		func() datasource.DataSource { return &dataSourceStandardsResourceScores{} },
 		func() datasource.DataSource { return &dataSourceStandardsResourcesScores{} },
 		func() datasource.DataSource { return &dataSourceStandards{} },
+		func() datasource.DataSource { return &dataSourceTag{} },
 	}
 }
 
 func (p *Provider) Resources(ctx context.Context) [](func() resource.Resource) {
 	return [](func() resource.Resource){
 		func() resource.Resource { return &resourceBusinessService{} },
+		func() resource.Resource { return &resourceTag{} },
 	}
 }
 
diff --git a/pagerdutyplugin/resource_pagerduty_tag.go b/pagerdutyplugin/resource_pagerduty_tag.go
new file mode 100644
index 000000000..6a9d75f3b
--- /dev/null
+++ b/pagerdutyplugin/resource_pagerduty_tag.go
@@ -0,0 +1,167 @@
+package pagerduty
+
+import (
+	"context"
+	"errors"
+	"log"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+)
+
+type resourceTag struct {
+	client *pagerduty.Client
+}
+
+var (
+	_ resource.ResourceWithConfigure   = (*resourceTag)(nil)
+	_ resource.ResourceWithImportState = (*resourceTag)(nil)
+)
+
+func (r *resourceTag) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...)
+}
+
+func (r *resourceTag) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = "pagerduty_tag"
+}
+
+func (r *resourceTag) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"label": schema.StringAttribute{
+				Required: true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"html_url": schema.StringAttribute{Computed: true},
+			"id":       schema.StringAttribute{Computed: true},
+			"summary":  schema.StringAttribute{Computed: true},
+		},
+	}
+}
+
+func (r *resourceTag) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var model resourceTagModel
+	if d := req.Config.Get(ctx, &model); d.HasError() {
+		resp.Diagnostics.Append(d...)
+	}
+	tagBody := buildTag(&model)
+	log.Printf("[INFO] Creating PagerDuty tag %s", tagBody.Label)
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		tag, err := r.client.CreateTagWithContext(ctx, tagBody)
+		if err != nil {
+			var apiErr pagerduty.APIError
+			if errors.As(err, &apiErr) && apiErr.StatusCode == 400 {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		model = flattenTag(tag)
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError("Error calling CreateTagWithContext", err.Error())
+	}
+	resp.State.Set(ctx, &model)
+}
+
+func (r *resourceTag) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var tagID types.String
+	if d := req.State.GetAttribute(ctx, path.Root("id"), &tagID); d.HasError() {
+		resp.Diagnostics.Append(d...)
+	}
+	log.Printf("[INFO] Reading PagerDuty tag %s", tagID)
+
+	var model resourceTagModel
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		tag, err := r.client.GetTagWithContext(ctx, tagID.ValueString())
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			if util.IsNotFoundError(err) {
+				log.Printf("[WARN] Removing %s because it's gone", tagID.String())
+				resp.State.RemoveResource(ctx)
+				return nil
+			}
+			return retry.RetryableError(err)
+		}
+		model = flattenTag(tag)
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError("Error calling GetTagWithContext", err.Error())
+	}
+	resp.State.Set(ctx, &model)
+}
+
+func (r *resourceTag) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+}
+
+func (r *resourceTag) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var model resourceTagModel
+	if d := req.State.Get(ctx, &model); d.HasError() {
+		resp.Diagnostics.Append(d...)
+	}
+	log.Printf("[INFO] Removing PagerDuty tag %s", model.ID)
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		err := r.client.DeleteTagWithContext(ctx, model.ID.ValueString())
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			if util.IsNotFoundError(err) {
+				resp.State.RemoveResource(ctx)
+				return nil
+			}
+			return retry.RetryableError(err)
+		}
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError("Error calling DeleteTagWithContext", err.Error())
+		return
+	}
+	resp.State.RemoveResource(ctx)
+}
+
+func (r *resourceTag) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+type resourceTagModel struct {
+	ID      types.String `tfsdk:"id"`
+	HtmlUrl types.String `tfsdk:"html_url"`
+	Label   types.String `tfsdk:"label"`
+	Summary types.String `tfsdk:"summary"`
+}
+
+func buildTag(model *resourceTagModel) *pagerduty.Tag {
+	tag := &pagerduty.Tag{
+		Label: model.Label.ValueString(),
+	}
+	tag.Type = "tag"
+	return tag
+}
+
+func flattenTag(tag *pagerduty.Tag) resourceTagModel {
+	model := resourceTagModel{
+		ID:      types.StringValue(tag.ID),
+		HtmlUrl: types.StringValue(tag.HTMLURL),
+		Label:   types.StringValue(tag.Label),
+		Summary: types.StringValue(tag.Summary),
+	}
+	return model
+}
diff --git a/pagerduty/resource_pagerduty_tag_test.go b/pagerdutyplugin/resource_pagerduty_tag_test.go
similarity index 75%
rename from pagerduty/resource_pagerduty_tag_test.go
rename to pagerdutyplugin/resource_pagerduty_tag_test.go
index 5e2f91d33..f6d317324 100644
--- a/pagerduty/resource_pagerduty_tag_test.go
+++ b/pagerdutyplugin/resource_pagerduty_tag_test.go
@@ -1,15 +1,16 @@
 package pagerduty
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"strings"
 	"testing"
 
+	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
-	"github.com/heimweh/go-pagerduty/pagerduty"
 )
 
 func init() {
@@ -20,17 +21,10 @@ func init() {
 }
 
 func testSweepTag(region string) error {
-	config, err := sharedConfigForRegion(region)
-	if err != nil {
-		return err
-	}
-
-	client, err := config.Client()
-	if err != nil {
-		return err
-	}
+	client := testAccProvider.client
+	ctx := context.Background()
 
-	resp, _, err := client.Tags.List(&pagerduty.ListTagsOptions{})
+	resp, err := client.ListTags(pagerduty.ListTagOptions{})
 	if err != nil {
 		return err
 	}
@@ -38,7 +32,7 @@ func testSweepTag(region string) error {
 	for _, tag := range resp.Tags {
 		if strings.HasPrefix(tag.Label, "test") || strings.HasPrefix(tag.Label, "tf-") {
 			log.Printf("Destroying tag %s (%s)", tag.Label, tag.ID)
-			if _, err := client.Tags.Delete(tag.ID); err != nil {
+			if err := client.DeleteTagWithContext(ctx, tag.ID); err != nil {
 				return err
 			}
 		}
@@ -51,9 +45,9 @@ func TestAccPagerDutyTag_Basic(t *testing.T) {
 	tagLabel := fmt.Sprintf("tf-%s", acctest.RandString(5))
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagConfig(tagLabel),
@@ -77,12 +71,13 @@ func TestAccPagerDutyTag_Basic(t *testing.T) {
 }
 
 func testAccCheckPagerDutyTagDestroy(s *terraform.State) error {
-	client, _ := testAccProvider.Meta().(*Config).Client()
+	client := testAccProvider.client
+	ctx := context.Background()
 	for _, r := range s.RootModule().Resources {
 		if r.Type != "pagerduty_tag" {
 			continue
 		}
-		if _, _, err := client.Tags.Get(r.Primary.ID); err == nil {
+		if _, err := client.GetTagWithContext(ctx, r.Primary.ID); err == nil {
 			return fmt.Errorf("Tag still exists")
 		}
 	}
@@ -91,6 +86,9 @@ func testAccCheckPagerDutyTagDestroy(s *terraform.State) error {
 
 func testAccCheckPagerDutyTagExists(n string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
+		client := testAccProvider.client
+		ctx := context.Background()
+
 		rs, ok := s.RootModule().Resources[n]
 		if !ok {
 			return fmt.Errorf("Not found: %s", n)
@@ -99,8 +97,7 @@ func testAccCheckPagerDutyTagExists(n string) resource.TestCheckFunc {
 			return fmt.Errorf("No Tag ID is set")
 		}
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
-		found, _, err := client.Tags.Get(rs.Primary.ID)
+		found, err := client.GetTagWithContext(ctx, rs.Primary.ID)
 		if err != nil {
 			return err
 		}
@@ -114,6 +111,9 @@ func testAccCheckPagerDutyTagExists(n string) resource.TestCheckFunc {
 
 func testAccExternallyDestroyTag(n string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
+		client := testAccProvider.client
+		ctx := context.Background()
+
 		rs, ok := s.RootModule().Resources[n]
 		if !ok {
 			return fmt.Errorf("Not found: %s", n)
@@ -122,9 +122,7 @@ func testAccExternallyDestroyTag(n string) resource.TestCheckFunc {
 			return fmt.Errorf("No Tag ID is set")
 		}
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
-		_, err := client.Tags.Delete(rs.Primary.ID)
-		if err != nil {
+		if err := client.DeleteTagWithContext(ctx, rs.Primary.ID); err != nil {
 			return err
 		}
 

From 3f7c358bf3cc37cd88f8dd1074e8cd62550cb216 Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Fri, 8 Mar 2024 13:31:49 -0300
Subject: [PATCH 04/10] Migrate resource tag assigment

---
 pagerduty/provider.go                         |   5 +-
 .../resource_pagerduty_tag_assignment.go      | 270 ---------------
 .../import_pagerduty_tag_assignment_test.go   |   6 +-
 pagerdutyplugin/provider.go                   |   1 +
 .../resource_pagerduty_tag_assignment.go      | 320 ++++++++++++++++++
 .../resource_pagerduty_tag_assignment_test.go |  49 ++-
 6 files changed, 348 insertions(+), 303 deletions(-)
 delete mode 100644 pagerduty/resource_pagerduty_tag_assignment.go
 rename {pagerduty => pagerdutyplugin}/import_pagerduty_tag_assignment_test.go (83%)
 create mode 100644 pagerdutyplugin/resource_pagerduty_tag_assignment.go
 rename {pagerduty => pagerdutyplugin}/resource_pagerduty_tag_assignment_test.go (86%)

diff --git a/pagerduty/provider.go b/pagerduty/provider.go
index 92b256e86..f6b5896df 100644
--- a/pagerduty/provider.go
+++ b/pagerduty/provider.go
@@ -127,7 +127,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_business_service":                              resourcePagerDutyBusinessService(),
 			"pagerduty_service_dependency":                            resourcePagerDutyServiceDependency(),
 			"pagerduty_response_play":                                 resourcePagerDutyResponsePlay(),
-			"pagerduty_tag_assignment":                                resourcePagerDutyTagAssignment(),
 			"pagerduty_service_event_rule":                            resourcePagerDutyServiceEventRule(),
 			"pagerduty_slack_connection":                              resourcePagerDutySlackConnection(),
 			"pagerduty_business_service_subscriber":                   resourcePagerDutyBusinessServiceSubscriber(),
@@ -212,7 +211,7 @@ func handleNotFoundError(err error, d *schema.ResourceData) error {
 	return genError(err, d)
 }
 
-func providerConfigureContextFunc(ctx context.Context, data *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) {
+func providerConfigureContextFunc(_ context.Context, data *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) {
 	var diags diag.Diagnostics
 	serviceRegion := strings.ToLower(data.Get("service_region").(string))
 
@@ -242,7 +241,7 @@ func providerConfigureContextFunc(ctx context.Context, data *schema.ResourceData
 		if err := validateAuthMethodConfig(data); err != nil {
 			diag := diag.Diagnostic{
 				Severity: diag.Warning,
-				Summary:  fmt.Sprint("`token` and `use_app_oauth_scoped_token` are both configured at the same time"),
+				Summary:  "`token` and `use_app_oauth_scoped_token` are both configured at the same time",
 				Detail:   err.Error(),
 			}
 			diags = append(diags, diag)
diff --git a/pagerduty/resource_pagerduty_tag_assignment.go b/pagerduty/resource_pagerduty_tag_assignment.go
deleted file mode 100644
index 84c986210..000000000
--- a/pagerduty/resource_pagerduty_tag_assignment.go
+++ /dev/null
@@ -1,270 +0,0 @@
-package pagerduty
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-func resourcePagerDutyTagAssignment() *schema.Resource {
-	return &schema.Resource{
-		Create: resourcePagerDutyTagAssignmentCreate,
-		Read:   resourcePagerDutyTagAssignmentRead,
-		Delete: resourcePagerDutyTagAssignmentDelete,
-		Importer: &schema.ResourceImporter{
-			State: resourcePagerDutyTagAssignmentImport,
-		},
-		Schema: map[string]*schema.Schema{
-			"entity_type": {
-				Type:     schema.TypeString,
-				Required: true,
-				ForceNew: true,
-				ValidateDiagFunc: validateValueDiagFunc([]string{
-					"users",
-					"teams",
-					"escalation_policies",
-				}),
-			},
-			"entity_id": {
-				Type:     schema.TypeString,
-				Required: true,
-				ForceNew: true,
-			},
-			"tag_id": {
-				Type:     schema.TypeString,
-				Required: true,
-				ForceNew: true,
-			},
-		},
-	}
-}
-
-func buildTagAssignmentStruct(d *schema.ResourceData) *pagerduty.TagAssignment {
-	assignment := &pagerduty.TagAssignment{
-		// Hard-coding "tag_reference" here because using the "tag" type doesn't allow users to delete the tags as
-		// they receive no tag id from the PagerDuty API at this time
-		Type:       "tag_reference",
-		EntityType: d.Get("entity_type").(string),
-		EntityID:   d.Get("entity_id").(string),
-	}
-	if attr, ok := d.GetOk("tag_id"); ok {
-		assignment.TagID = attr.(string)
-	}
-
-	return assignment
-}
-
-func resourcePagerDutyTagAssignmentCreate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	assignment := buildTagAssignmentStruct(d)
-	assignments := &pagerduty.TagAssignments{
-		Add: []*pagerduty.TagAssignment{assignment},
-	}
-
-	log.Printf("[INFO] Creating PagerDuty tag assignment with tagID %s for %s entity with ID %s", assignment.TagID, assignment.EntityType, assignment.EntityID)
-
-	retryErr := retry.Retry(5*time.Minute, func() *retry.RetryError {
-		if _, err := client.Tags.Assign(assignment.EntityType, assignment.EntityID, assignments); err != nil {
-			if isErrCode(err, 400) {
-				return retry.NonRetryableError(err)
-			}
-
-			return retry.RetryableError(err)
-		} else {
-			// create tag_assignment id using the entityID.tagID as PagerDuty API does not return one
-			assignmentID := createAssignmentID(assignment.EntityID, assignment.TagID)
-			d.SetId(assignmentID)
-		}
-		return nil
-	})
-
-	if retryErr != nil {
-		return retryErr
-	}
-	// give PagerDuty 2 seconds to save the assignment correctly
-	time.Sleep(2 * time.Second)
-	return resourcePagerDutyTagAssignmentRead(d, meta)
-}
-
-func resourcePagerDutyTagAssignmentRead(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	assignment := buildTagAssignmentStruct(d)
-
-	log.Printf("[INFO] Reading PagerDuty tag assignment with tagID %s for %s entity with ID %s", assignment.TagID, assignment.EntityType, assignment.EntityID)
-
-	ok, err := isFoundTagAssignmentEntity(d.Get("entity_id").(string), d.Get("entity_type").(string), meta)
-	if err != nil {
-		return err
-	}
-	if !ok {
-		d.SetId("")
-		return nil
-	}
-
-	return retry.Retry(2*time.Minute, func() *retry.RetryError {
-		if tagResponse, _, err := client.Tags.ListTagsForEntity(assignment.EntityType, assignment.EntityID); err != nil {
-			if isErrCode(err, http.StatusBadRequest) {
-				return retry.NonRetryableError(err)
-			}
-
-			time.Sleep(2 * time.Second)
-			return retry.RetryableError(err)
-		} else if tagResponse != nil {
-			var foundTag *pagerduty.Tag
-
-			// loop tags and find matching ID
-			for _, tag := range tagResponse.Tags {
-				if tag.ID == assignment.TagID {
-					foundTag = tag
-					break
-				}
-			}
-			if foundTag == nil {
-				d.SetId("")
-				return nil
-			}
-		}
-		return nil
-	})
-}
-
-func resourcePagerDutyTagAssignmentDelete(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	assignment := buildTagAssignmentStruct(d)
-	assignments := &pagerduty.TagAssignments{
-		Remove: []*pagerduty.TagAssignment{assignment},
-	}
-	log.Printf("[INFO] Deleting PagerDuty tag assignment with tagID %s for entityID %s", assignment.TagID, assignment.EntityID)
-
-	retryErr := retry.Retry(2*time.Minute, func() *retry.RetryError {
-		if _, err := client.Tags.Assign(assignment.EntityType, assignment.EntityID, assignments); err != nil {
-			if isErrCode(err, 404) || isMalformedNotFoundError(err) {
-				return nil
-			}
-			if isErrCode(err, 400) {
-				return retry.NonRetryableError(err)
-			}
-
-			return retry.RetryableError(err)
-		}
-
-		return nil
-	})
-
-	if retryErr != nil {
-		return retryErr
-	}
-	d.SetId("")
-
-	return nil
-}
-
-func resourcePagerDutyTagAssignmentImport(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_tag_assignment. Expecting an importation ID formed as '<entity_type>.<entity_id>.<tag_id>'")
-	}
-	entityType, entityID, tagID := ids[0], ids[1], ids[2]
-
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return []*schema.ResourceData{}, err
-	}
-
-	// give PagerDuty 2 seconds to save the assignment correctly
-	time.Sleep(2 * time.Second)
-	tagResponse, _, err := client.Tags.ListTagsForEntity(entityType, entityID)
-	if err != nil {
-		return []*schema.ResourceData{}, fmt.Errorf("error importing pagerduty_tag_assignment: %s", err.Error())
-	}
-	var foundTag *pagerduty.Tag
-	// loop tags and find matching ID
-	for _, tag := range tagResponse.Tags {
-		if tag.ID == tagID {
-			// create tag_assignment id using the entityID.tagID as PagerDuty API does not return one
-			assignmentID := createAssignmentID(entityID, tagID)
-			d.SetId(assignmentID)
-			d.Set("entity_id", entityID)
-			d.Set("entity_type", entityType)
-			d.Set("tag_id", tagID)
-			foundTag = tag
-			break
-		}
-	}
-	if foundTag == nil {
-		d.SetId("")
-		return []*schema.ResourceData{}, fmt.Errorf("error importing pagerduty_tag_assignment. Tag not found for entity")
-	}
-
-	return []*schema.ResourceData{d}, err
-}
-
-func isFoundTagAssignmentEntity(entityID, entityType string, meta interface{}) (bool, error) {
-	var isFound bool
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return isFound, err
-	}
-
-	fetchUser := func(id string) (*pagerduty.User, *pagerduty.Response, error) {
-		return client.Users.Get(id, &pagerduty.GetUserOptions{})
-	}
-	fetchTeam := func(id string) (*pagerduty.Team, *pagerduty.Response, error) {
-		return client.Teams.Get(id)
-	}
-	fetchEscalationPolicy := func(id string) (*pagerduty.EscalationPolicy, *pagerduty.Response, error) {
-		return client.EscalationPolicies.Get(id, &pagerduty.GetEscalationPolicyOptions{})
-	}
-	retryErr := retry.Retry(2*time.Minute, func() *retry.RetryError {
-		var err error
-		if entityType == "users" {
-			_, _, err = fetchUser(entityID)
-		}
-		if entityType == "teams" {
-			_, _, err = fetchTeam(entityID)
-		}
-		if entityType == "escalation_policies" {
-			_, _, err = fetchEscalationPolicy(entityID)
-		}
-
-		if err != nil && isErrCode(err, http.StatusNotFound) {
-			return nil
-		}
-		if err != nil {
-			return retry.RetryableError(err)
-		}
-		if isErrCode(err, http.StatusBadRequest) {
-			return retry.NonRetryableError(err)
-		}
-
-		isFound = true
-
-		return nil
-	})
-	if retryErr != nil {
-		return isFound, retryErr
-	}
-	return isFound, nil
-}
-
-func createAssignmentID(entityID, tagID string) string {
-	return fmt.Sprintf("%v.%v", entityID, tagID)
-}
diff --git a/pagerduty/import_pagerduty_tag_assignment_test.go b/pagerdutyplugin/import_pagerduty_tag_assignment_test.go
similarity index 83%
rename from pagerduty/import_pagerduty_tag_assignment_test.go
rename to pagerdutyplugin/import_pagerduty_tag_assignment_test.go
index cd125d2bd..c0b79668a 100644
--- a/pagerduty/import_pagerduty_tag_assignment_test.go
+++ b/pagerdutyplugin/import_pagerduty_tag_assignment_test.go
@@ -14,9 +14,9 @@ func TestAccPagerDutyTagAssignment_import(t *testing.T) {
 	team := fmt.Sprintf("tf-%s", acctest.RandString(5))
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagAssignmentDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagAssignmentDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagAssignmentTeamConfig(tag, team),
diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go
index 3b55a9349..f31ec37a1 100644
--- a/pagerdutyplugin/provider.go
+++ b/pagerdutyplugin/provider.go
@@ -61,6 +61,7 @@ func (p *Provider) Resources(ctx context.Context) [](func() resource.Resource) {
 	return [](func() resource.Resource){
 		func() resource.Resource { return &resourceBusinessService{} },
 		func() resource.Resource { return &resourceTag{} },
+		func() resource.Resource { return &resourceTagAssignment{} },
 	}
 }
 
diff --git a/pagerdutyplugin/resource_pagerduty_tag_assignment.go b/pagerdutyplugin/resource_pagerduty_tag_assignment.go
new file mode 100644
index 000000000..e886d48fd
--- /dev/null
+++ b/pagerdutyplugin/resource_pagerduty_tag_assignment.go
@@ -0,0 +1,320 @@
+package pagerduty
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"strings"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"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/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+)
+
+type resourceTagAssignment struct{ client *pagerduty.Client }
+
+var (
+	_ resource.ResourceWithConfigure   = (*resourceTagAssignment)(nil)
+	_ resource.ResourceWithImportState = (*resourceTagAssignment)(nil)
+)
+
+func (r *resourceTagAssignment) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = "pagerduty_tag_assignment"
+}
+
+func (r *resourceTagAssignment) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Computed:      true,
+				PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+			},
+			"entity_type": schema.StringAttribute{
+				Required:      true,
+				PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+				Validators: []validator.String{
+					stringvalidator.OneOf("users", "teams", "escalation_policies"),
+				},
+			},
+			"entity_id": schema.StringAttribute{
+				Required:      true,
+				PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+			},
+			"tag_id": schema.StringAttribute{
+				Required:      true,
+				PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+			},
+		},
+	}
+}
+
+func (r *resourceTagAssignment) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var model resourceTagAssignmentModel
+
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	assign := buildTagAssignment(&model)
+	log.Printf("[INFO] Creating PagerDuty tag assignment with tagID %s for %s entity with ID %s", assign.TagID, assign.EntityType, assign.EntityID)
+
+	assignments := &pagerduty.TagAssignments{
+		Add: []*pagerduty.TagAssignment{
+			{Type: "tag_reference", TagID: assign.TagID},
+		},
+	}
+
+	err := retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError {
+		err := r.client.AssignTagsWithContext(ctx, assign.EntityType, assign.EntityID, assignments)
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		model.ID = flattenTagAssignmentID(assign.EntityID, assign.TagID)
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error creating PagerDuty tag assignment with tagID %s for %s entity with ID %s", assign.TagID, assign.EntityType, assign.EntityID),
+			err.Error(),
+		)
+		return
+	}
+
+	isFound := r.requestGetTagAssignents(ctx, model, &resp.Diagnostics)
+	if !isFound {
+		resp.State.RemoveResource(ctx)
+		return
+	}
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+func (r *resourceTagAssignment) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state resourceTagAssignmentModel
+
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	log.Printf("[INFO] Reading PagerDuty tag assignment %s", state.ID)
+
+	isFound := r.requestGetTagAssignents(ctx, state, &resp.Diagnostics)
+	if !isFound {
+		resp.State.RemoveResource(ctx)
+		return
+	}
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *resourceTagAssignment) requestGetTagAssignents(ctx context.Context, model resourceTagAssignmentModel, diags *diag.Diagnostics) bool {
+	assign := buildTagAssignment(&model)
+
+	isFound := r.isFoundTagAssignment(ctx, assign.EntityType, assign.EntityID, diags)
+	if !isFound {
+		return false
+	}
+
+	isFound = false
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		opts := pagerduty.ListTagOptions{}
+		response, err := r.client.GetTagsForEntity(assign.EntityType, assign.EntityID, opts)
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		for _, tag := range response.Tags {
+			if tag.ID == assign.TagID {
+				isFound = true
+				break
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		diags.AddError(
+			fmt.Sprintf("Error reading tags for %s entity with ID %s", assign.EntityType, assign.EntityID),
+			err.Error(),
+		)
+	}
+	if !isFound {
+		return false
+	}
+	return true
+}
+
+func (r *resourceTagAssignment) isFoundTagAssignment(ctx context.Context, entityType, entityID string, diags *diag.Diagnostics) bool {
+	isFound := false
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		var err error
+
+		switch entityType {
+		case "users":
+			opts := pagerduty.GetUserOptions{}
+			_, err = r.client.GetUserWithContext(ctx, entityID, opts)
+		case "teams":
+			_, err = r.client.GetTeamWithContext(ctx, entityID)
+		case "escalation_policies":
+			opts := pagerduty.GetEscalationPolicyOptions{}
+			_, err = r.client.GetEscalationPolicyWithContext(ctx, entityID, &opts)
+		}
+
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			if util.IsNotFoundError(err) {
+				return nil
+			}
+			return retry.RetryableError(err)
+		}
+
+		isFound = true
+		return nil
+	})
+	if err != nil {
+		diags.AddError(
+			fmt.Sprintf("Error finding %s entity with ID %s", entityType, entityID),
+			err.Error(),
+		)
+		isFound = false
+	}
+
+	return isFound
+}
+
+func (r *resourceTagAssignment) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+}
+
+func (r *resourceTagAssignment) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var model resourceTagAssignmentModel
+
+	resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	assign := buildTagAssignment(&model)
+	log.Printf("[INFO] Deleting PagerDuty tag assignment with tagID %s for entityID %s", assign.TagID, assign.EntityID)
+
+	assignments := &pagerduty.TagAssignments{
+		Remove: []*pagerduty.TagAssignment{
+			{Type: "tag_reference", TagID: assign.TagID},
+		},
+	}
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		err := r.client.AssignTagsWithContext(ctx, assign.EntityType, assign.EntityID, assignments)
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			if util.IsNotFoundError(err) {
+				return nil
+			}
+			return retry.RetryableError(err)
+		}
+		model.ID = flattenTagAssignmentID(assign.EntityID, assign.TagID)
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error creating PagerDuty tag assignment with tagID %s for %s entity with ID %s", assign.TagID, assign.EntityType, assign.EntityID),
+			err.Error(),
+		)
+		return
+	}
+	resp.State.RemoveResource(ctx)
+}
+
+func (r *resourceTagAssignment) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...)
+}
+
+func (r *resourceTagAssignment) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	// resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+	ids := strings.Split(req.ID, ".")
+	if len(ids) != 3 {
+		resp.Diagnostics.AddError(
+			"Error importing pagerduty_tag_assignment",
+			"Expecting an importation ID formed as '<entity_type>.<entity_id>.<tag_id>'",
+		)
+		return
+	}
+	entityType, entityID, tagID := ids[0], ids[1], ids[2]
+
+	tagResponse, err := r.client.GetTagsForEntity(entityType, entityID, pagerduty.ListTagOptions{})
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing pagerduty_tag_assignment",
+			err.Error(),
+		)
+	}
+
+	isFound := false
+	for _, tag := range tagResponse.Tags {
+		if tag.ID == tagID {
+			isFound = true
+			break
+		}
+	}
+	if !isFound {
+		resp.State.RemoveResource(ctx)
+		resp.Diagnostics.AddError("Error importing pagerduty_tag_assignment", "Tag not found for entity")
+		return
+	}
+
+	state := flattenTagAssignment(entityType, entityID, tagID)
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+type resourceTagAssignmentModel struct {
+	ID         types.String `tfsdk:"id"`
+	EntityID   types.String `tfsdk:"entity_id"`
+	EntityType types.String `tfsdk:"entity_type"`
+	TagID      types.String `tfsdk:"tag_id"`
+}
+
+type tagAssignment struct {
+	ID         string
+	EntityID   string
+	EntityType string
+	TagID      string
+}
+
+func buildTagAssignment(model *resourceTagAssignmentModel) tagAssignment {
+	return tagAssignment{
+		ID:         model.ID.ValueString(),
+		EntityID:   model.EntityID.ValueString(),
+		EntityType: model.EntityType.ValueString(),
+		TagID:      model.TagID.ValueString(),
+	}
+}
+
+func flattenTagAssignment(entityType, entityID, tagID string) resourceTagAssignmentModel {
+	model := resourceTagAssignmentModel{
+		ID:         flattenTagAssignmentID(entityID, tagID),
+		EntityID:   types.StringValue(entityID),
+		EntityType: types.StringValue(entityType),
+		TagID:      types.StringValue(tagID),
+	}
+	return model
+}
+
+func flattenTagAssignmentID(entityID, tagID string) types.String {
+	// TODO: i think this should have entityType for consistency with import
+	return types.StringValue(fmt.Sprintf("%v.%v", entityID, tagID))
+}
diff --git a/pagerduty/resource_pagerduty_tag_assignment_test.go b/pagerdutyplugin/resource_pagerduty_tag_assignment_test.go
similarity index 86%
rename from pagerduty/resource_pagerduty_tag_assignment_test.go
rename to pagerdutyplugin/resource_pagerduty_tag_assignment_test.go
index a7b9aa723..e4c4af901 100644
--- a/pagerduty/resource_pagerduty_tag_assignment_test.go
+++ b/pagerdutyplugin/resource_pagerduty_tag_assignment_test.go
@@ -1,14 +1,15 @@
 package pagerduty
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"testing"
 
+	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
-	"github.com/heimweh/go-pagerduty/pagerduty"
 )
 
 func TestAccPagerDutyTagAssignment_User(t *testing.T) {
@@ -17,9 +18,9 @@ func TestAccPagerDutyTagAssignment_User(t *testing.T) {
 	email := fmt.Sprintf("%s@foo.test", username)
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagAssignmentDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagAssignmentDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagAssignmentConfig(tagLabel, username, email),
@@ -62,14 +63,15 @@ func TestAccPagerDutyTagAssignment_User(t *testing.T) {
 		},
 	})
 }
+
 func TestAccPagerDutyTagAssignment_Team(t *testing.T) {
 	tagLabel := fmt.Sprintf("tf-%s", acctest.RandString(5))
 	team := fmt.Sprintf("tf-%s", acctest.RandString(5))
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagAssignmentDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagAssignmentDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagAssignmentTeamConfig(tagLabel, team),
@@ -110,6 +112,7 @@ func TestAccPagerDutyTagAssignment_Team(t *testing.T) {
 		},
 	})
 }
+
 func TestAccPagerDutyTagAssignment_EP(t *testing.T) {
 	tagLabel := fmt.Sprintf("tf-%s", acctest.RandString(5))
 	ep := fmt.Sprintf("tf-%s", acctest.RandString(5))
@@ -117,9 +120,9 @@ func TestAccPagerDutyTagAssignment_EP(t *testing.T) {
 	email := fmt.Sprintf("%s@foo.test", username)
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyTagAssignmentDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyTagAssignmentDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyTagAssignmentEPConfig(tagLabel, username, email, ep),
@@ -162,7 +165,6 @@ func TestAccPagerDutyTagAssignment_EP(t *testing.T) {
 }
 
 func testAccCheckPagerDutyTagAssignmentDestroy(s *terraform.State) error {
-	client, _ := testAccProvider.Meta().(*Config).Client()
 	for _, r := range s.RootModule().Resources {
 		if r.Type != "pagerduty_tag_assignment" {
 			continue
@@ -172,7 +174,8 @@ func testAccCheckPagerDutyTagAssignmentDestroy(s *terraform.State) error {
 		entityID, tagID := ids[0], ids[1]
 		entityType := "users"
 
-		response, _, err := client.Tags.ListTagsForEntity(entityType, entityID)
+		opts := pagerduty.ListTagOptions{}
+		response, err := testAccProvider.client.GetTagsForEntity(entityType, entityID, opts)
 		if err != nil {
 			// if there are no tags for the entity that's okay
 			return nil
@@ -200,8 +203,8 @@ func testAccCheckPagerDutyTagAssignmentExists(n, entityType string) resource.Tes
 
 		entityID, tagID := ids[0], ids[1]
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
-		response, _, err := client.Tags.ListTagsForEntity(entityType, entityID)
+		opts := pagerduty.ListTagOptions{}
+		response, err := testAccProvider.client.GetTagsForEntity(entityType, entityID, opts)
 		if err != nil {
 			return err
 		}
@@ -287,15 +290,6 @@ resource "pagerduty_tag_assignment" "foo" {
 }
 
 func testAccExternallyDestroyTagAssignment(n, entityType string) resource.TestCheckFunc {
-	deleteUser := func(id string, client *pagerduty.Client) (*pagerduty.Response, error) {
-		return client.Users.Delete(id)
-	}
-	deleteTeam := func(id string, client *pagerduty.Client) (*pagerduty.Response, error) {
-		return client.Teams.Delete(id)
-	}
-	deleteEscalationPolicy := func(id string, client *pagerduty.Client) (*pagerduty.Response, error) {
-		return client.EscalationPolicies.Delete(id)
-	}
 	return func(s *terraform.State) error {
 		rs, ok := s.RootModule().Resources[n]
 		if !ok {
@@ -305,16 +299,17 @@ func testAccExternallyDestroyTagAssignment(n, entityType string) resource.TestCh
 			return fmt.Errorf("No Tag Assignment ID is set")
 		}
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
+		ctx := context.Background()
+
 		var err error
 		if entityType == "users" {
-			_, err = deleteUser(rs.Primary.ID, client)
+			err = testAccProvider.client.DeleteUserWithContext(ctx, rs.Primary.ID)
 		}
 		if entityType == "teams" {
-			_, err = deleteTeam(rs.Primary.ID, client)
+			err = testAccProvider.client.DeleteTeamWithContext(ctx, rs.Primary.ID)
 		}
 		if entityType == "escalation_policies" {
-			_, err = deleteEscalationPolicy(rs.Primary.ID, client)
+			err = testAccProvider.client.DeleteEscalationPolicyWithContext(ctx, rs.Primary.ID)
 		}
 		if err != nil {
 			return err

From 090394da953e5524261b27338060527962a34990 Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Thu, 29 Feb 2024 01:54:38 -0300
Subject: [PATCH 05/10] Migrate resource pagerduty_extension

---
 go.mod                                        |   1 +
 go.sum                                        |   2 +
 pagerduty/provider.go                         |   1 -
 pagerduty/resource_pagerduty_extension.go     | 253 ------------
 .../import_pagerduty_extension_test.go        |   6 +-
 pagerdutyplugin/provider.go                   |   3 +-
 .../resource_pagerduty_business_service.go    |   2 +-
 .../resource_pagerduty_extension.go           | 363 +++++++++++++++++
 .../resource_pagerduty_extension_test.go      |  32 +-
 .../LICENSE                                   | 375 ++++++++++++++++++
 .../jsontypes/doc.go                          |   5 +
 .../jsontypes/exact_type.go                   | 128 ++++++
 .../jsontypes/exact_value.go                  |  91 +++++
 .../jsontypes/normalized_type.go              | 128 ++++++
 .../jsontypes/normalized_value.go             | 164 ++++++++
 .../resource/schema/setplanmodifier/doc.go    |   5 +
 .../setplanmodifier/requires_replace.go       |  30 ++
 .../setplanmodifier/requires_replace_if.go    |  73 ++++
 .../requires_replace_if_configured.go         |  34 ++
 .../requires_replace_if_func.go               |  25 ++
 .../setplanmodifier/use_state_for_unknown.go  |  55 +++
 vendor/modules.txt                            |   4 +
 22 files changed, 1501 insertions(+), 279 deletions(-)
 delete mode 100644 pagerduty/resource_pagerduty_extension.go
 rename {pagerduty => pagerdutyplugin}/import_pagerduty_extension_test.go (78%)
 create mode 100644 pagerdutyplugin/resource_pagerduty_extension.go
 rename {pagerduty => pagerdutyplugin}/resource_pagerduty_extension_test.go (86%)
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/LICENSE
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/doc.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_type.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_value.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_type.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_value.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/doc.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_configured.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_func.go
 create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/use_state_for_unknown.go

diff --git a/go.mod b/go.mod
index ff8bb4f38..3345e789c 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/hashicorp/terraform-exec v0.20.0
 	github.com/hashicorp/terraform-json v0.20.0
 	github.com/hashicorp/terraform-plugin-framework v1.5.0
+	github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0
 	github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
 	github.com/hashicorp/terraform-plugin-go v0.20.0
 	github.com/hashicorp/terraform-plugin-mux v0.13.0
diff --git a/go.sum b/go.sum
index 294c2d538..ffc58c1a5 100644
--- a/go.sum
+++ b/go.sum
@@ -75,6 +75,8 @@ github.com/hashicorp/terraform-json v0.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+
 github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
 github.com/hashicorp/terraform-plugin-framework v1.5.0 h1:8kcvqJs/x6QyOFSdeAyEgsenVOUeC/IyKpi2ul4fjTg=
 github.com/hashicorp/terraform-plugin-framework v1.5.0/go.mod h1:6waavirukIlFpVpthbGd2PUNYaFedB0RwW3MDzJ/rtc=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0 h1:b8vZYB/SkXJT4YPbT3trzE6oJ7dPyMy68+9dEDKsJjE=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0/go.mod h1:tP9BC3icoXBz72evMS5UTFvi98CiKhPdXF6yLs1wS8A=
 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
 github.com/hashicorp/terraform-plugin-go v0.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ=
diff --git a/pagerduty/provider.go b/pagerduty/provider.go
index f6b5896df..b21c2bb2c 100644
--- a/pagerduty/provider.go
+++ b/pagerduty/provider.go
@@ -119,7 +119,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_user":                                          resourcePagerDutyUser(),
 			"pagerduty_user_contact_method":                           resourcePagerDutyUserContactMethod(),
 			"pagerduty_user_notification_rule":                        resourcePagerDutyUserNotificationRule(),
-			"pagerduty_extension":                                     resourcePagerDutyExtension(),
 			"pagerduty_extension_servicenow":                          resourcePagerDutyExtensionServiceNow(),
 			"pagerduty_event_rule":                                    resourcePagerDutyEventRule(),
 			"pagerduty_ruleset":                                       resourcePagerDutyRuleset(),
diff --git a/pagerduty/resource_pagerduty_extension.go b/pagerduty/resource_pagerduty_extension.go
deleted file mode 100644
index cd76c61da..000000000
--- a/pagerduty/resource_pagerduty_extension.go
+++ /dev/null
@@ -1,253 +0,0 @@
-package pagerduty
-
-import (
-	"encoding/json"
-	"fmt"
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-func resourcePagerDutyExtension() *schema.Resource {
-	return &schema.Resource{
-		Create: resourcePagerDutyExtensionCreate,
-		Read:   resourcePagerDutyExtensionRead,
-		Update: resourcePagerDutyExtensionUpdate,
-		Delete: resourcePagerDutyExtensionDelete,
-		Importer: &schema.ResourceImporter{
-			State: resourcePagerDutyExtensionImport,
-		},
-		Schema: map[string]*schema.Schema{
-			"name": {
-				Type:     schema.TypeString,
-				Optional: true,
-				Computed: true,
-			},
-			"html_url": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-			"type": {
-				Type:     schema.TypeString,
-				Optional: true,
-				Computed: true,
-			},
-			"endpoint_url": {
-				Type:      schema.TypeString,
-				Optional:  true,
-				Sensitive: true,
-			},
-			"extension_objects": {
-				Type:     schema.TypeSet,
-				Required: true,
-				ForceNew: true,
-				Elem:     &schema.Schema{Type: schema.TypeString},
-			},
-			"extension_schema": {
-				Type:     schema.TypeString,
-				ForceNew: true,
-				Required: true,
-			},
-			"config": {
-				Type:             schema.TypeString,
-				Optional:         true,
-				ValidateFunc:     validation.StringIsJSON,
-				DiffSuppressFunc: structure.SuppressJsonDiff,
-			},
-			"summary": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-		},
-	}
-}
-
-func buildExtensionStruct(d *schema.ResourceData) *pagerduty.Extension {
-	Extension := &pagerduty.Extension{
-		Name:        d.Get("name").(string),
-		Type:        "extension",
-		EndpointURL: d.Get("endpoint_url").(string),
-		ExtensionSchema: &pagerduty.ExtensionSchemaReference{
-			Type: "extension_schema_reference",
-			ID:   d.Get("extension_schema").(string),
-		},
-		ExtensionObjects: expandServiceObjects(d.Get("extension_objects")),
-	}
-
-	if v, ok := d.GetOk("config"); ok {
-		Extension.Config = expandExtensionConfig(v)
-	}
-
-	return Extension
-}
-
-func fetchPagerDutyExtension(d *schema.ResourceData, meta interface{}, errCallback func(error, *schema.ResourceData) error) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	return retry.Retry(2*time.Minute, func() *retry.RetryError {
-		extension, _, err := client.Extensions.Get(d.Id())
-		if err != nil {
-			if isErrCode(err, http.StatusBadRequest) {
-				return retry.NonRetryableError(err)
-			}
-
-			errResp := errCallback(err, d)
-			if errResp != nil {
-				time.Sleep(2 * time.Second)
-				return retry.RetryableError(errResp)
-			}
-
-			return nil
-		}
-
-		d.Set("summary", extension.Summary)
-		d.Set("name", extension.Name)
-		d.Set("endpoint_url", extension.EndpointURL)
-		d.Set("html_url", extension.HTMLURL)
-		if err := d.Set("extension_objects", flattenExtensionObjects(extension.ExtensionObjects)); err != nil {
-			log.Printf("[WARN] error setting extension_objects: %s", err)
-		}
-		d.Set("extension_schema", extension.ExtensionSchema.ID)
-
-		if err := d.Set("config", flattenExtensionConfig(extension.Config)); err != nil {
-			log.Printf("[WARN] error setting extension config: %s", err)
-		}
-
-		return nil
-	})
-}
-
-func resourcePagerDutyExtensionCreate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	extension := buildExtensionStruct(d)
-
-	log.Printf("[INFO] Creating PagerDuty extension %s", extension.Name)
-
-	extension, _, err = client.Extensions.Create(extension)
-	if err != nil {
-		return err
-	}
-
-	d.SetId(extension.ID)
-
-	return fetchPagerDutyExtension(d, meta, genError)
-}
-
-func resourcePagerDutyExtensionRead(d *schema.ResourceData, meta interface{}) error {
-	log.Printf("[INFO] Reading PagerDuty extension %s", d.Id())
-	return fetchPagerDutyExtension(d, meta, handleNotFoundError)
-}
-
-func resourcePagerDutyExtensionUpdate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	extension := buildExtensionStruct(d)
-
-	log.Printf("[INFO] Updating PagerDuty extension %s", d.Id())
-
-	if _, _, err := client.Extensions.Update(d.Id(), extension); err != nil {
-		return err
-	}
-
-	return resourcePagerDutyExtensionRead(d, meta)
-}
-
-func resourcePagerDutyExtensionDelete(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Deleting PagerDuty extension %s", d.Id())
-
-	if _, err := client.Extensions.Delete(d.Id()); err != nil {
-		if perr, ok := err.(*pagerduty.Error); ok && perr.Code == 5001 {
-			log.Printf("[WARN] Extension (%s) not found, removing from state", d.Id())
-			return nil
-		}
-		return err
-	}
-
-	d.SetId("")
-
-	return nil
-}
-
-func resourcePagerDutyExtensionImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return []*schema.ResourceData{}, err
-	}
-
-	extension, _, err := client.Extensions.Get(d.Id())
-	if err != nil {
-		return []*schema.ResourceData{}, fmt.Errorf("error importing pagerduty_extension. Expecting an importation ID for extension")
-	}
-
-	d.Set("endpoint_url", extension.EndpointURL)
-	d.Set("extension_objects", []string{extension.ExtensionObjects[0].ID})
-	d.Set("extension_schema", extension.ExtensionSchema.ID)
-
-	return []*schema.ResourceData{d}, err
-}
-
-func expandServiceObjects(v interface{}) []*pagerduty.ServiceReference {
-	var services []*pagerduty.ServiceReference
-
-	for _, srv := range v.(*schema.Set).List() {
-		service := &pagerduty.ServiceReference{
-			Type: "service_reference",
-			ID:   srv.(string),
-		}
-		services = append(services, service)
-	}
-
-	return services
-}
-
-func flattenExtensionObjects(serviceList []*pagerduty.ServiceReference) interface{} {
-	var services []interface{}
-	for _, s := range serviceList {
-		// only flatten service_reference types, because that's all we send at this
-		// time
-		if s.Type == "service_reference" {
-			services = append(services, s.ID)
-		}
-	}
-	return services
-}
-
-func expandExtensionConfig(v interface{}) interface{} {
-	var config interface{}
-	if err := json.Unmarshal([]byte(v.(string)), &config); err != nil {
-		log.Printf("[ERROR] Could not unmarshal extension config %s: %v", v.(string), err)
-		return nil
-	}
-
-	return config
-}
-
-func flattenExtensionConfig(config interface{}) interface{} {
-	json, err := json.Marshal(config)
-	if err != nil {
-		log.Printf("[ERROR] Could not marshal extension config %s: %v", config.(string), err)
-		return nil
-	}
-	return string(json)
-}
diff --git a/pagerduty/import_pagerduty_extension_test.go b/pagerdutyplugin/import_pagerduty_extension_test.go
similarity index 78%
rename from pagerduty/import_pagerduty_extension_test.go
rename to pagerdutyplugin/import_pagerduty_extension_test.go
index 6db27db79..82fe5bae8 100644
--- a/pagerduty/import_pagerduty_extension_test.go
+++ b/pagerdutyplugin/import_pagerduty_extension_test.go
@@ -14,9 +14,9 @@ func TestAccPagerDutyExtension_import(t *testing.T) {
 	url := "https://example.com/receive_a_pagerduty_webhook"
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyExtensionDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyExtensionDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyExtensionConfig(name, extension_name, url, "false", "any"),
diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go
index f31ec37a1..1a5560132 100644
--- a/pagerdutyplugin/provider.go
+++ b/pagerdutyplugin/provider.go
@@ -60,8 +60,9 @@ 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 &resourceTag{} },
+		func() resource.Resource { return &resourceExtension{} },
 		func() resource.Resource { return &resourceTagAssignment{} },
+		func() resource.Resource { return &resourceTag{} },
 	}
 }
 
diff --git a/pagerdutyplugin/resource_pagerduty_business_service.go b/pagerdutyplugin/resource_pagerduty_business_service.go
index e322f2527..58781947d 100644
--- a/pagerdutyplugin/resource_pagerduty_business_service.go
+++ b/pagerdutyplugin/resource_pagerduty_business_service.go
@@ -202,7 +202,7 @@ func requestGetBusinessService(ctx context.Context, client *pagerduty.Client, id
 	return model
 }
 
-func buildPagerdutyBusinessService(ctx context.Context, model *resourceBusinessServiceModel) *pagerduty.BusinessService {
+func buildPagerdutyBusinessService(_ context.Context, model *resourceBusinessServiceModel) *pagerduty.BusinessService {
 	businessService := pagerduty.BusinessService{
 		ID:             model.ID.ValueString(),
 		Description:    model.Description.ValueString(),
diff --git a/pagerdutyplugin/resource_pagerduty_extension.go b/pagerdutyplugin/resource_pagerduty_extension.go
new file mode 100644
index 000000000..476653e0f
--- /dev/null
+++ b/pagerdutyplugin/resource_pagerduty_extension.go
@@ -0,0 +1,363 @@
+package pagerduty
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+
+	"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+)
+
+type resourceExtension struct{ client *pagerduty.Client }
+
+var (
+	_ resource.ResourceWithConfigure   = (*resourceExtension)(nil)
+	_ resource.ResourceWithImportState = (*resourceExtension)(nil)
+)
+
+func (r *resourceExtension) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = "pagerduty_extension"
+}
+
+func (r *resourceExtension) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"name":         schema.StringAttribute{Optional: true, Computed: true},
+			"id":           schema.StringAttribute{Computed: true},
+			"html_url":     schema.StringAttribute{Computed: true},
+			"type":         schema.StringAttribute{Optional: true, Computed: true},
+			"endpoint_url": schema.StringAttribute{Optional: true, Sensitive: true},
+			"summary":      schema.StringAttribute{Computed: true},
+			"extension_objects": schema.SetAttribute{
+				Required:    true,
+				ElementType: types.StringType,
+				PlanModifiers: []planmodifier.Set{
+					setplanmodifier.RequiresReplace(),
+				},
+			},
+			"extension_schema": schema.StringAttribute{
+				Required: true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"config": schema.StringAttribute{
+				Optional:   true,
+				Computed:   true,
+				CustomType: jsontypes.NormalizedType{},
+			},
+		},
+	}
+}
+
+func (r *resourceExtension) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var model resourceExtensionModel
+
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	plan := buildPagerdutyExtension(ctx, &model, &resp.Diagnostics)
+	log.Printf("[INFO] Creating PagerDuty extension %s", plan.Name)
+
+	extension, err := r.client.CreateExtensionWithContext(ctx, plan)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error creating extension %s", plan.Name),
+			err.Error(),
+		)
+		return
+	}
+	plan.ID = extension.ID
+
+	accessToken := buildExtensionConfigAccessToken(model.Config, &resp.Diagnostics)
+	model = requestGetExtension(ctx, r.client, plan.ID, accessToken, &resp.Diagnostics)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+func (r *resourceExtension) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state resourceExtensionModel
+
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	log.Printf("[INFO] Reading PagerDuty extension %s", state.ID)
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		extension, err := r.client.GetExtensionWithContext(ctx, state.ID.ValueString())
+		if err != nil {
+			if util.IsBadRequestError(err) || util.IsNotFoundError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		accessToken := buildExtensionConfigAccessToken(state.Config, &resp.Diagnostics)
+		state = flattenExtension(extension, accessToken, &resp.Diagnostics)
+		return nil
+	})
+	if err != nil {
+		if util.IsNotFoundError(err) {
+			resp.State.RemoveResource(ctx)
+			return
+		}
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error reading extension %s", state.ID),
+			err.Error(),
+		)
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *resourceExtension) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var model resourceExtensionModel
+
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	plan := buildPagerdutyExtension(ctx, &model, &resp.Diagnostics)
+	if plan.ID == "" {
+		var id string
+		req.State.GetAttribute(ctx, path.Root("id"), &id)
+		plan.ID = id
+	}
+	log.Printf("[INFO] Updating PagerDuty extension %s", plan.ID)
+
+	_, err := r.client.UpdateExtensionWithContext(ctx, plan.ID, plan)
+	if err != nil {
+		if util.IsNotFoundError(err) {
+			resp.State.RemoveResource(ctx)
+			return
+		}
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error updating extension %s", plan.ID),
+			err.Error(),
+		)
+		return
+	}
+
+	accessToken := buildExtensionConfigAccessToken(model.Config, &resp.Diagnostics)
+	model = requestGetExtension(ctx, r.client, plan.ID, accessToken, &resp.Diagnostics)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+func (r *resourceExtension) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var id types.String
+
+	resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	log.Printf("[INFO] Deleting PagerDuty extension %s", id)
+
+	err := r.client.DeleteExtensionWithContext(ctx, id.ValueString())
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error deleting extension %s", id),
+			err.Error(),
+		)
+		return
+	}
+	resp.State.RemoveResource(ctx)
+}
+
+func (r *resourceExtension) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...)
+}
+
+func (r *resourceExtension) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	model := requestGetExtension(ctx, r.client, req.ID, nil, &resp.Diagnostics)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+type resourceExtensionModel struct {
+	Name             types.String         `tfsdk:"name"`
+	Config           jsontypes.Normalized `tfsdk:"config"`
+	EndpointURL      types.String         `tfsdk:"endpoint_url"`
+	ExtensionObjects types.Set            `tfsdk:"extension_objects"`
+	ExtensionSchema  types.String         `tfsdk:"extension_schema"`
+	HTMLURL          types.String         `tfsdk:"html_url"`
+	ID               types.String         `tfsdk:"id"`
+	Summary          types.String         `tfsdk:"summary"`
+	Type             types.String         `tfsdk:"type"`
+}
+
+func requestGetExtension(ctx context.Context, client *pagerduty.Client, id string, accessToken *string, diags *diag.Diagnostics) resourceExtensionModel {
+	var model resourceExtensionModel
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		extension, err := client.GetExtensionWithContext(ctx, id)
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		model = flattenExtension(extension, accessToken, diags)
+		return nil
+	})
+	if err != nil {
+		diags.AddError(
+			fmt.Sprintf("Error reading extension %s", id),
+			err.Error(),
+		)
+	}
+	return model
+}
+
+func buildPagerdutyExtension(ctx context.Context, model *resourceExtensionModel, diags *diag.Diagnostics) *pagerduty.Extension {
+	extension := pagerduty.Extension{
+		Name:             model.Name.ValueString(),
+		Config:           buildExtensionConfig(model.Config, diags),
+		EndpointURL:      model.EndpointURL.ValueString(),
+		ExtensionObjects: buildExtensionObjects(ctx, model.ExtensionObjects, diags),
+		ExtensionSchema:  buildExtensionSchema(model.ExtensionSchema),
+	}
+	extension.ID = model.ID.ValueString()
+	extension.Type = "extension"
+	return &extension
+}
+
+func buildExtensionSchema(s types.String) pagerduty.APIObject {
+	if s.IsNull() || s.IsUnknown() {
+		return pagerduty.APIObject{}
+	}
+	return pagerduty.APIObject{Type: "extension_schema_reference", ID: s.ValueString()}
+}
+
+func buildExtensionObjects(ctx context.Context, list types.Set, diags *diag.Diagnostics) []pagerduty.APIObject {
+	if list.IsNull() || list.IsUnknown() {
+		return nil
+	}
+
+	values := make([]string, 0, len(list.Elements()))
+	d := list.ElementsAs(ctx, &values, false)
+	diags.Append(d...)
+
+	objects := make([]pagerduty.APIObject, 0, len(values))
+	for _, v := range values {
+		objects = append(objects, pagerduty.APIObject{
+			ID:   v,
+			Type: "service_reference",
+		})
+	}
+
+	return objects
+}
+
+func buildExtensionConfig(s jsontypes.Normalized, diags *diag.Diagnostics) interface{} {
+	if s.IsNull() || s.IsUnknown() {
+		return nil
+	}
+
+	var config interface{}
+	if err := json.Unmarshal([]byte(s.ValueString()), &config); err != nil {
+		diags.AddError(
+			"Could not unmarshal extension config",
+			fmt.Sprintf("%v\nIn value %s", err, s),
+		)
+		return nil
+	}
+
+	return config
+}
+
+func buildExtensionConfigAccessToken(s jsontypes.Normalized, diags *diag.Diagnostics) *string {
+	if s.IsNull() || s.IsUnknown() {
+		return nil
+	}
+
+	var config interface{}
+	if err := json.Unmarshal([]byte(s.ValueString()), &config); err != nil {
+		diags.AddError(
+			"Could not unmarshal extension config",
+			fmt.Sprintf("%v\nIn value %s", err, s),
+		)
+		return nil
+	}
+
+	if c, ok := config.(map[string]interface{}); ok {
+		if v, ok := c["access_token"].(string); ok {
+			return &v
+		}
+	}
+	return nil
+}
+
+func flattenExtension(response *pagerduty.Extension, accessToken *string, diags *diag.Diagnostics) resourceExtensionModel {
+	model := resourceExtensionModel{
+		ID:               types.StringValue(response.ID),
+		Name:             types.StringValue(response.Name),
+		HTMLURL:          types.StringValue(response.HTMLURL),
+		Summary:          types.StringValue(response.Summary),
+		EndpointURL:      types.StringValue(response.EndpointURL),
+		Config:           flattenExtensionConfig(response.Config, accessToken, diags),
+		ExtensionSchema:  types.StringValue(response.ExtensionSchema.ID),
+		ExtensionObjects: flattenExtensionObjects(response.ExtensionObjects, diags),
+	}
+	return model
+}
+
+func flattenExtensionConfig(config interface{}, accessToken *string, diags *diag.Diagnostics) jsontypes.Normalized {
+	if c, ok := config.(map[string]interface{}); ok {
+		if accessToken == nil {
+			delete(c, "access_token")
+		} else {
+			c["access_token"] = *accessToken
+		}
+		config = c
+	}
+
+	buf, err := json.Marshal(config)
+	if err != nil {
+		diags.AddError(
+			"Could not marshal extension config",
+			fmt.Sprintf("%v\n%v", err, config),
+		)
+		return jsontypes.NewNormalizedNull()
+	}
+	return jsontypes.NewNormalizedValue(string(buf))
+}
+
+func flattenExtensionObjects(objects []pagerduty.APIObject, diags *diag.Diagnostics) types.Set {
+	values := []attr.Value{}
+	for _, o := range objects {
+		// only flatten service_reference types, because that's all we
+		// send at this time
+		if o.Type == "service_reference" {
+			values = append(values, types.StringValue(o.ID))
+		}
+	}
+
+	list, d := types.SetValue(types.StringType, values)
+	diags.Append(d...)
+	return list
+}
diff --git a/pagerduty/resource_pagerduty_extension_test.go b/pagerdutyplugin/resource_pagerduty_extension_test.go
similarity index 86%
rename from pagerduty/resource_pagerduty_extension_test.go
rename to pagerdutyplugin/resource_pagerduty_extension_test.go
index 207154b20..0ce11de0b 100644
--- a/pagerduty/resource_pagerduty_extension_test.go
+++ b/pagerdutyplugin/resource_pagerduty_extension_test.go
@@ -1,15 +1,16 @@
 package pagerduty
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"strings"
 	"testing"
 
+	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
-	"github.com/heimweh/go-pagerduty/pagerduty"
 )
 
 func init() {
@@ -20,17 +21,9 @@ func init() {
 }
 
 func testSweepExtension(region string) error {
-	config, err := sharedConfigForRegion(region)
-	if err != nil {
-		return err
-	}
-
-	client, err := config.Client()
-	if err != nil {
-		return err
-	}
+	ctx := context.Background()
 
-	resp, _, err := client.Extensions.List(&pagerduty.ListExtensionsOptions{})
+	resp, err := testAccProvider.client.ListExtensionsWithContext(ctx, pagerduty.ListExtensionOptions{})
 	if err != nil {
 		return err
 	}
@@ -38,7 +31,7 @@ func testSweepExtension(region string) error {
 	for _, extension := range resp.Extensions {
 		if strings.HasPrefix(extension.Name, "test") || strings.HasPrefix(extension.Name, "tf-") {
 			log.Printf("Destroying extension %s (%s)", extension.Name, extension.ID)
-			if _, err := client.Extensions.Delete(extension.ID); err != nil {
+			if err := testAccProvider.client.DeleteExtensionWithContext(ctx, extension.ID); err != nil {
 				return err
 			}
 		}
@@ -55,9 +48,9 @@ func TestAccPagerDutyExtension_Basic(t *testing.T) {
 	url_updated := "https://example.com/webhook_foo"
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyExtensionDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyExtensionDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyExtensionConfig(name, extension_name, url, "false", "any"),
@@ -94,13 +87,13 @@ func TestAccPagerDutyExtension_Basic(t *testing.T) {
 }
 
 func testAccCheckPagerDutyExtensionDestroy(s *terraform.State) error {
-	client, _ := testAccProvider.Meta().(*Config).Client()
 	for _, r := range s.RootModule().Resources {
 		if r.Type != "pagerduty_extension" {
 			continue
 		}
 
-		if _, _, err := client.Extensions.Get(r.Primary.ID); err == nil {
+		ctx := context.Background()
+		if _, err := testAccProvider.client.GetExtensionWithContext(ctx, r.Primary.ID); err == nil {
 			return fmt.Errorf("Extension still exists")
 		}
 
@@ -119,9 +112,8 @@ func testAccCheckPagerDutyExtensionExists(n string) resource.TestCheckFunc {
 			return fmt.Errorf("No extension ID is set")
 		}
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
-
-		found, _, err := client.Extensions.Get(rs.Primary.ID)
+		ctx := context.Background()
+		found, err := testAccProvider.client.GetExtensionWithContext(ctx, rs.Primary.ID)
 		if err != nil {
 			return err
 		}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/LICENSE b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/LICENSE
new file mode 100644
index 000000000..0efb0a530
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/LICENSE
@@ -0,0 +1,375 @@
+Copyright (c) 2023 HashiCorp, Inc.
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/doc.go
new file mode 100644
index 000000000..e2800494f
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/doc.go
@@ -0,0 +1,5 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+// Package jsontypes contains Terraform Plugin Framework Custom Type implementations for JSON formatted strings (RFC 7159).
+package jsontypes
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_type.go b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_type.go
new file mode 100644
index 000000000..6861f3155
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_type.go
@@ -0,0 +1,128 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package jsontypes
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+	"github.com/hashicorp/terraform-plugin-go/tftypes"
+)
+
+var (
+	_ basetypes.StringTypable = (*ExactType)(nil)
+	_ xattr.TypeWithValidate  = (*ExactType)(nil)
+)
+
+// ExactType is an attribute type that represents a valid JSON string (RFC 7159). No semantic equality logic is defined for ExactType,
+// so it will follow Terraform's data-consistency rules for strings, which must match byte-for-byte. Consider using NormalizedType
+// to allow inconsequential differences between JSON strings (whitespace, property order, etc).
+type ExactType struct {
+	basetypes.StringType
+}
+
+// String returns a human readable string of the type name.
+func (t ExactType) String() string {
+	return "jsontypes.ExactType"
+}
+
+// ValueType returns the Value type.
+func (t ExactType) ValueType(ctx context.Context) attr.Value {
+	return Exact{}
+}
+
+// Equal returns true if the given type is equivalent.
+func (t ExactType) Equal(o attr.Type) bool {
+	other, ok := o.(ExactType)
+
+	if !ok {
+		return false
+	}
+
+	return t.StringType.Equal(other.StringType)
+}
+
+// Validate implements type validation. This type requires the value provided to be a String value that is valid JSON format (RFC 7159).
+func (t ExactType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics {
+	var diags diag.Diagnostics
+
+	if in.Type() == nil {
+		return diags
+	}
+
+	if !in.Type().Is(tftypes.String) {
+		err := fmt.Errorf("expected String value, received %T with value: %v", in, in)
+		diags.AddAttributeError(
+			path,
+			"JSON Exact Type Validation Error",
+			"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+
+				"Please report the following to the provider developer:\n\n"+err.Error(),
+		)
+		return diags
+	}
+
+	if !in.IsKnown() || in.IsNull() {
+		return diags
+	}
+
+	var valueString string
+
+	if err := in.As(&valueString); err != nil {
+		diags.AddAttributeError(
+			path,
+			"JSON Exact Type Validation Error",
+			"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+
+				"Please report the following to the provider developer:\n\n"+err.Error(),
+		)
+
+		return diags
+	}
+
+	if ok := json.Valid([]byte(valueString)); !ok {
+		diags.AddAttributeError(
+			path,
+			"Invalid JSON String Value",
+			"A string value was provided that is not valid JSON string format (RFC 7159).\n\n"+
+				"Given Value: "+valueString+"\n",
+		)
+
+		return diags
+	}
+
+	return diags
+}
+
+// ValueFromString returns a StringValuable type given a StringValue.
+func (t ExactType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
+	return Exact{
+		StringValue: in,
+	}, nil
+}
+
+// ValueFromTerraform returns a Value given a tftypes.Value.  This is meant to convert the tftypes.Value into a more convenient Go type
+// for the provider to consume the data with.
+func (t ExactType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+	attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
+	if err != nil {
+		return nil, err
+	}
+
+	stringValue, ok := attrValue.(basetypes.StringValue)
+	if !ok {
+		return nil, fmt.Errorf("unexpected value type of %T", attrValue)
+	}
+
+	stringValuable, diags := t.ValueFromString(ctx, stringValue)
+	if diags.HasError() {
+		return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
+	}
+
+	return stringValuable, nil
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_value.go b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_value.go
new file mode 100644
index 000000000..effd2157c
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/exact_value.go
@@ -0,0 +1,91 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package jsontypes
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+var (
+	_ basetypes.StringValuable = (*Exact)(nil)
+)
+
+// Exact represents a valid JSON string (RFC 7159). No semantic equality logic is defined for Exact,
+// so it will follow Terraform's data-consistency rules for strings, which must match byte-for-byte.
+// Consider using Normalized to allow inconsequential differences between JSON strings (whitespace, property order, etc).
+type Exact struct {
+	basetypes.StringValue
+}
+
+// Type returns an ExactType.
+func (v Exact) Type(_ context.Context) attr.Type {
+	return ExactType{}
+}
+
+// Equal returns true if the given value is equivalent.
+func (v Exact) Equal(o attr.Value) bool {
+	other, ok := o.(Exact)
+
+	if !ok {
+		return false
+	}
+
+	return v.StringValue.Equal(other.StringValue)
+}
+
+// Unmarshal calls (encoding/json).Unmarshal with the Exact StringValue and `target` input. A null or unknown value will produce an error diagnostic.
+// See encoding/json docs for more on usage: https://pkg.go.dev/encoding/json#Unmarshal
+func (v Exact) Unmarshal(target any) diag.Diagnostics {
+	var diags diag.Diagnostics
+
+	if v.IsNull() {
+		diags.Append(diag.NewErrorDiagnostic("Exact JSON Unmarshal Error", "json string value is null"))
+		return diags
+	}
+
+	if v.IsUnknown() {
+		diags.Append(diag.NewErrorDiagnostic("Exact JSON Unmarshal Error", "json string value is unknown"))
+		return diags
+	}
+
+	err := json.Unmarshal([]byte(v.ValueString()), target)
+	if err != nil {
+		diags.Append(diag.NewErrorDiagnostic("Exact JSON Unmarshal Error", err.Error()))
+	}
+
+	return diags
+}
+
+// NewExactNull creates an Exact with a null value. Determine whether the value is null via IsNull method.
+func NewExactNull() Exact {
+	return Exact{
+		StringValue: basetypes.NewStringNull(),
+	}
+}
+
+// NewExactUnknown creates an Exact with an unknown value. Determine whether the value is unknown via IsUnknown method.
+func NewExactUnknown() Exact {
+	return Exact{
+		StringValue: basetypes.NewStringUnknown(),
+	}
+}
+
+// NewExactValue creates an Exact with a known value. Access the value via ValueString method.
+func NewExactValue(value string) Exact {
+	return Exact{
+		StringValue: basetypes.NewStringValue(value),
+	}
+}
+
+// NewExactPointerValue creates an Exact with a null value if nil or a known value. Access the value via ValueStringPointer method.
+func NewExactPointerValue(value *string) Exact {
+	return Exact{
+		StringValue: basetypes.NewStringPointerValue(value),
+	}
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_type.go b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_type.go
new file mode 100644
index 000000000..0fca3645c
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_type.go
@@ -0,0 +1,128 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package jsontypes
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+	"github.com/hashicorp/terraform-plugin-go/tftypes"
+)
+
+var (
+	_ basetypes.StringTypable = (*NormalizedType)(nil)
+	_ xattr.TypeWithValidate  = (*NormalizedType)(nil)
+)
+
+// NormalizedType is an attribute type that represents a valid JSON string (RFC 7159). Semantic equality logic is defined for NormalizedType
+// such that inconsequential differences between JSON strings are ignored (whitespace, property order, etc). If you need strict, byte-for-byte,
+// string equality, consider using ExactType.
+type NormalizedType struct {
+	basetypes.StringType
+}
+
+// String returns a human readable string of the type name.
+func (t NormalizedType) String() string {
+	return "jsontypes.NormalizedType"
+}
+
+// ValueType returns the Value type.
+func (t NormalizedType) ValueType(ctx context.Context) attr.Value {
+	return Normalized{}
+}
+
+// Equal returns true if the given type is equivalent.
+func (t NormalizedType) Equal(o attr.Type) bool {
+	other, ok := o.(NormalizedType)
+
+	if !ok {
+		return false
+	}
+
+	return t.StringType.Equal(other.StringType)
+}
+
+// Validate implements type validation. This type requires the value provided to be a String value that is valid JSON format (RFC 7159).
+func (t NormalizedType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics {
+	var diags diag.Diagnostics
+
+	if in.Type() == nil {
+		return diags
+	}
+
+	if !in.Type().Is(tftypes.String) {
+		err := fmt.Errorf("expected String value, received %T with value: %v", in, in)
+		diags.AddAttributeError(
+			path,
+			"JSON Normalized Type Validation Error",
+			"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+
+				"Please report the following to the provider developer:\n\n"+err.Error(),
+		)
+		return diags
+	}
+
+	if !in.IsKnown() || in.IsNull() {
+		return diags
+	}
+
+	var valueString string
+
+	if err := in.As(&valueString); err != nil {
+		diags.AddAttributeError(
+			path,
+			"JSON Normalized Type Validation Error",
+			"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. "+
+				"Please report the following to the provider developer:\n\n"+err.Error(),
+		)
+
+		return diags
+	}
+
+	if ok := json.Valid([]byte(valueString)); !ok {
+		diags.AddAttributeError(
+			path,
+			"Invalid JSON String Value",
+			"A string value was provided that is not valid JSON string format (RFC 7159).\n\n"+
+				"Given Value: "+valueString+"\n",
+		)
+
+		return diags
+	}
+
+	return diags
+}
+
+// ValueFromString returns a StringValuable type given a StringValue.
+func (t NormalizedType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
+	return Normalized{
+		StringValue: in,
+	}, nil
+}
+
+// ValueFromTerraform returns a Value given a tftypes.Value.  This is meant to convert the tftypes.Value into a more convenient Go type
+// for the provider to consume the data with.
+func (t NormalizedType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
+	attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
+	if err != nil {
+		return nil, err
+	}
+
+	stringValue, ok := attrValue.(basetypes.StringValue)
+	if !ok {
+		return nil, fmt.Errorf("unexpected value type of %T", attrValue)
+	}
+
+	stringValuable, diags := t.ValueFromString(ctx, stringValue)
+	if diags.HasError() {
+		return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
+	}
+
+	return stringValuable, nil
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_value.go b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_value.go
new file mode 100644
index 000000000..897b96173
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes/normalized_value.go
@@ -0,0 +1,164 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package jsontypes
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+var (
+	_ basetypes.StringValuable                   = (*Normalized)(nil)
+	_ basetypes.StringValuableWithSemanticEquals = (*Normalized)(nil)
+)
+
+// Normalized represents a valid JSON string (RFC 7159). Semantic equality logic is defined for Normalized
+// such that inconsequential differences between JSON strings are ignored (whitespace, property order, etc). If you
+// need strict, byte-for-byte, string equality, consider using ExactType.
+type Normalized struct {
+	basetypes.StringValue
+}
+
+// Type returns a NormalizedType.
+func (v Normalized) Type(_ context.Context) attr.Type {
+	return NormalizedType{}
+}
+
+// Equal returns true if the given value is equivalent.
+func (v Normalized) Equal(o attr.Value) bool {
+	other, ok := o.(Normalized)
+
+	if !ok {
+		return false
+	}
+
+	return v.StringValue.Equal(other.StringValue)
+}
+
+// StringSemanticEquals returns true if the given JSON string value is semantically equal to the current JSON string value. When compared,
+// these JSON string values are "normalized" by marshalling them to empty Go structs. This prevents Terraform data consistency errors and
+// resource drift due to inconsequential differences in the JSON strings (whitespace, property order, etc).
+func (v Normalized) StringSemanticEquals(_ context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
+	var diags diag.Diagnostics
+
+	newValue, ok := newValuable.(Normalized)
+	if !ok {
+		diags.AddError(
+			"Semantic Equality Check Error",
+			"An unexpected value type was received while performing semantic equality checks. "+
+				"Please report this to the provider developers.\n\n"+
+				"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+
+				"Got Value Type: "+fmt.Sprintf("%T", newValuable),
+		)
+
+		return false, diags
+	}
+
+	result, err := jsonEqual(newValue.ValueString(), v.ValueString())
+
+	if err != nil {
+		diags.AddError(
+			"Semantic Equality Check Error",
+			"An unexpected error occurred while performing semantic equality checks. "+
+				"Please report this to the provider developers.\n\n"+
+				"Error: "+err.Error(),
+		)
+
+		return false, diags
+	}
+
+	return result, diags
+}
+
+func jsonEqual(s1, s2 string) (bool, error) {
+	s1, err := normalizeJSONString(s1)
+	if err != nil {
+		return false, err
+	}
+
+	s2, err = normalizeJSONString(s2)
+	if err != nil {
+		return false, err
+	}
+
+	return s1 == s2, nil
+}
+
+func normalizeJSONString(jsonStr string) (string, error) {
+	dec := json.NewDecoder(strings.NewReader(jsonStr))
+
+	// This ensures the JSON decoder will not parse JSON numbers into Go's float64 type; avoiding Go
+	// normalizing the JSON number representation or imposing limits on numeric range. See the unit test cases
+	// of StringSemanticEquals for examples.
+	dec.UseNumber()
+
+	var temp interface{}
+	if err := dec.Decode(&temp); err != nil {
+		return "", err
+	}
+
+	jsonBytes, err := json.Marshal(&temp)
+	if err != nil {
+		return "", err
+	}
+
+	return string(jsonBytes), nil
+}
+
+// Unmarshal calls (encoding/json).Unmarshal with the Normalized StringValue and `target` input. A null or unknown value will produce an error diagnostic.
+// See encoding/json docs for more on usage: https://pkg.go.dev/encoding/json#Unmarshal
+func (v Normalized) Unmarshal(target any) diag.Diagnostics {
+	var diags diag.Diagnostics
+
+	if v.IsNull() {
+		diags.Append(diag.NewErrorDiagnostic("Normalized JSON Unmarshal Error", "json string value is null"))
+		return diags
+	}
+
+	if v.IsUnknown() {
+		diags.Append(diag.NewErrorDiagnostic("Normalized JSON Unmarshal Error", "json string value is unknown"))
+		return diags
+	}
+
+	err := json.Unmarshal([]byte(v.ValueString()), target)
+	if err != nil {
+		diags.Append(diag.NewErrorDiagnostic("Normalized JSON Unmarshal Error", err.Error()))
+	}
+
+	return diags
+}
+
+// NewNormalizedNull creates a Normalized with a null value. Determine whether the value is null via IsNull method.
+func NewNormalizedNull() Normalized {
+	return Normalized{
+		StringValue: basetypes.NewStringNull(),
+	}
+}
+
+// NewNormalizedUnknown creates a Normalized with an unknown value. Determine whether the value is unknown via IsUnknown method.
+func NewNormalizedUnknown() Normalized {
+	return Normalized{
+		StringValue: basetypes.NewStringUnknown(),
+	}
+}
+
+// NewNormalizedValue creates a Normalized with a known value. Access the value via ValueString method.
+func NewNormalizedValue(value string) Normalized {
+	return Normalized{
+		StringValue: basetypes.NewStringValue(value),
+	}
+}
+
+// NewNormalizedPointerValue creates a Normalized with a null value if nil or a known value. Access the value via ValueStringPointer method.
+func NewNormalizedPointerValue(value *string) Normalized {
+	return Normalized{
+		StringValue: basetypes.NewStringPointerValue(value),
+	}
+}
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/doc.go
new file mode 100644
index 000000000..93363bb7f
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/doc.go
@@ -0,0 +1,5 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+// Package setplanmodifier provides plan modifiers for types.Set attributes.
+package setplanmodifier
diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace.go
new file mode 100644
index 000000000..4a2c4c5e2
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace.go
@@ -0,0 +1,30 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package setplanmodifier
+
+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.Set {
+	return RequiresReplaceIf(
+		func(_ context.Context, _ planmodifier.SetRequest, 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/setplanmodifier/requires_replace_if.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if.go
new file mode 100644
index 000000000..2a0109bb0
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if.go
@@ -0,0 +1,73 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package setplanmodifier
+
+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.Set {
+	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
+}
+
+// PlanModifySet implements the plan modification logic.
+func (m requiresReplaceIfModifier) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) {
+	// 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/setplanmodifier/requires_replace_if_configured.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_configured.go
new file mode 100644
index 000000000..c37cd5332
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_configured.go
@@ -0,0 +1,34 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package setplanmodifier
+
+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.Set {
+	return RequiresReplaceIf(
+		func(_ context.Context, req planmodifier.SetRequest, 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/setplanmodifier/requires_replace_if_func.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_func.go
new file mode 100644
index 000000000..9e2ada6f7
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/requires_replace_if_func.go
@@ -0,0 +1,25 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package setplanmodifier
+
+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.SetRequest, *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/setplanmodifier/use_state_for_unknown.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/use_state_for_unknown.go
new file mode 100644
index 000000000..0bf359cd2
--- /dev/null
+++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier/use_state_for_unknown.go
@@ -0,0 +1,55 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package setplanmodifier
+
+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.Set {
+	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."
+}
+
+// PlanModifySet implements the plan modification logic.
+func (m useStateForUnknownModifier) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) {
+	// 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 2a3ca2012..7c567239f 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -166,12 +166,16 @@ 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/planmodifier
+github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier
 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
 github.com/hashicorp/terraform-plugin-framework/types/basetypes
+# github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0
+## explicit; go 1.19
+github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes
 # github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
 ## explicit; go 1.19
 github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag

From 1a3effff0df9a0c6c0664a7b7f2fbde8d3b52b36 Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Wed, 6 Mar 2024 10:39:33 -0300
Subject: [PATCH 06/10] Migrate resource extension servicenow

---
 pagerduty/provider.go                         |   1 -
 ...resource_pagerduty_extension_servicenow.go | 274 --------------
 ...ort_pagerduty_extension_servicenow_test.go |   0
 pagerdutyplugin/provider.go                   |  14 +
 ...resource_pagerduty_extension_servicenow.go | 357 ++++++++++++++++++
 ...rce_pagerduty_extension_servicenow_test.go |   0
 6 files changed, 371 insertions(+), 275 deletions(-)
 delete mode 100644 pagerduty/resource_pagerduty_extension_servicenow.go
 rename {pagerduty => pagerdutyplugin}/import_pagerduty_extension_servicenow_test.go (100%)
 create mode 100644 pagerdutyplugin/resource_pagerduty_extension_servicenow.go
 rename {pagerduty => pagerdutyplugin}/resource_pagerduty_extension_servicenow_test.go (100%)

diff --git a/pagerduty/provider.go b/pagerduty/provider.go
index b21c2bb2c..410725768 100644
--- a/pagerduty/provider.go
+++ b/pagerduty/provider.go
@@ -119,7 +119,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_user":                                          resourcePagerDutyUser(),
 			"pagerduty_user_contact_method":                           resourcePagerDutyUserContactMethod(),
 			"pagerduty_user_notification_rule":                        resourcePagerDutyUserNotificationRule(),
-			"pagerduty_extension_servicenow":                          resourcePagerDutyExtensionServiceNow(),
 			"pagerduty_event_rule":                                    resourcePagerDutyEventRule(),
 			"pagerduty_ruleset":                                       resourcePagerDutyRuleset(),
 			"pagerduty_ruleset_rule":                                  resourcePagerDutyRulesetRule(),
diff --git a/pagerduty/resource_pagerduty_extension_servicenow.go b/pagerduty/resource_pagerduty_extension_servicenow.go
deleted file mode 100644
index 56efbb3d0..000000000
--- a/pagerduty/resource_pagerduty_extension_servicenow.go
+++ /dev/null
@@ -1,274 +0,0 @@
-package pagerduty
-
-import (
-	"encoding/json"
-	"fmt"
-	"log"
-	"net/http"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-type PagerDutyExtensionServiceNowConfig struct {
-	User        string `json:"snow_user"`
-	Password    string `json:"snow_password,omitempty"`
-	SyncOptions string `json:"sync_options"`
-	Target      string `json:"target"`
-	TaskType    string `json:"task_type"`
-	Referer     string `json:"referer"`
-}
-
-func resourcePagerDutyExtensionServiceNow() *schema.Resource {
-	return &schema.Resource{
-		Create: resourcePagerDutyExtensionServiceNowCreate,
-		Read:   resourcePagerDutyExtensionServiceNowRead,
-		Update: resourcePagerDutyExtensionServiceNowUpdate,
-		Delete: resourcePagerDutyExtensionServiceNowDelete,
-		Importer: &schema.ResourceImporter{
-			State: resourcePagerDutyExtensionServiceNowImport,
-		},
-		Schema: map[string]*schema.Schema{
-			"name": {
-				Type:     schema.TypeString,
-				Optional: true,
-				Computed: true,
-			},
-			"html_url": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-			"type": {
-				Type:     schema.TypeString,
-				Optional: true,
-				Computed: true,
-			},
-			"endpoint_url": {
-				Type:      schema.TypeString,
-				Optional:  true,
-				Sensitive: true,
-			},
-			"extension_objects": {
-				Type:     schema.TypeSet,
-				Required: true,
-				ForceNew: true,
-				Elem:     &schema.Schema{Type: schema.TypeString},
-			},
-			"extension_schema": {
-				Type:     schema.TypeString,
-				ForceNew: true,
-				Required: true,
-			},
-			"snow_user": {
-				Type:     schema.TypeString,
-				Required: true,
-			},
-			"snow_password": {
-				Type:      schema.TypeString,
-				Required:  true,
-				Sensitive: true,
-			},
-			"summary": {
-				Type:     schema.TypeString,
-				Optional: true,
-				Computed: true,
-			},
-			"sync_options": {
-				Type:         schema.TypeString,
-				Required:     true,
-				ValidateFunc: validation.StringInSlice([]string{"manual_sync", "sync_all"}, false),
-			},
-			"target": {
-				Type:     schema.TypeString,
-				Required: true,
-			},
-			"task_type": {
-				Type:     schema.TypeString,
-				Required: true,
-			},
-			"referer": {
-				Type:     schema.TypeString,
-				Required: true,
-			},
-		},
-	}
-}
-
-func buildExtensionServiceNowStruct(d *schema.ResourceData) *pagerduty.Extension {
-	Extension := &pagerduty.Extension{
-		Name:        d.Get("name").(string),
-		Type:        "extension",
-		EndpointURL: d.Get("endpoint_url").(string),
-		ExtensionSchema: &pagerduty.ExtensionSchemaReference{
-			Type: "extension_schema_reference",
-			ID:   d.Get("extension_schema").(string),
-		},
-		ExtensionObjects: expandServiceNowServiceObjects(d.Get("extension_objects")),
-	}
-
-	config := &PagerDutyExtensionServiceNowConfig{
-		User:        d.Get("snow_user").(string),
-		Password:    d.Get("snow_password").(string),
-		SyncOptions: d.Get("sync_options").(string),
-		Target:      d.Get("target").(string),
-		TaskType:    d.Get("task_type").(string),
-		Referer:     d.Get("referer").(string),
-	}
-	Extension.Config = config
-
-	return Extension
-}
-
-func fetchPagerDutyExtensionServiceNowCreate(d *schema.ResourceData, meta interface{}, errCallback func(error, *schema.ResourceData) error) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	return retry.Retry(2*time.Minute, func() *retry.RetryError {
-		extension, _, err := client.Extensions.Get(d.Id())
-		if err != nil {
-			if isErrCode(err, http.StatusBadRequest) {
-				return retry.NonRetryableError(err)
-			}
-
-			errResp := errCallback(err, d)
-			if errResp != nil {
-				time.Sleep(2 * time.Second)
-				return retry.RetryableError(errResp)
-			}
-
-			return nil
-		}
-
-		d.Set("summary", extension.Summary)
-		d.Set("name", extension.Name)
-		d.Set("endpoint_url", extension.EndpointURL)
-		d.Set("html_url", extension.HTMLURL)
-		if err := d.Set("extension_objects", flattenExtensionServiceNowObjects(extension.ExtensionObjects)); err != nil {
-			log.Printf("[WARN] error setting extension_objects: %s", err)
-		}
-		d.Set("extension_schema", extension.ExtensionSchema.ID)
-
-		b, _ := json.Marshal(extension.Config)
-		config := new(PagerDutyExtensionServiceNowConfig)
-		json.Unmarshal(b, config)
-		d.Set("snow_user", config.User)
-		d.Set("snow_password", config.Password)
-		d.Set("sync_options", config.SyncOptions)
-		d.Set("target", config.Target)
-		d.Set("task_type", config.TaskType)
-		d.Set("referer", config.Referer)
-
-		return nil
-	})
-}
-
-func resourcePagerDutyExtensionServiceNowCreate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	extension := buildExtensionServiceNowStruct(d)
-
-	log.Printf("[INFO] Creating PagerDuty extension %s", extension.Name)
-
-	extension, _, err = client.Extensions.Create(extension)
-	if err != nil {
-		return err
-	}
-
-	d.SetId(extension.ID)
-	return fetchPagerDutyExtensionServiceNowCreate(d, meta, genError)
-}
-
-func resourcePagerDutyExtensionServiceNowRead(d *schema.ResourceData, meta interface{}) error {
-	log.Printf("[INFO] Reading PagerDuty extension %s", d.Id())
-	return fetchPagerDutyExtensionServiceNowCreate(d, meta, handleNotFoundError)
-}
-
-func resourcePagerDutyExtensionServiceNowUpdate(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	extension := buildExtensionServiceNowStruct(d)
-
-	log.Printf("[INFO] Updating PagerDuty extension %s", d.Id())
-
-	if _, _, err := client.Extensions.Update(d.Id(), extension); err != nil {
-		return err
-	}
-
-	return resourcePagerDutyExtensionServiceNowRead(d, meta)
-}
-
-func resourcePagerDutyExtensionServiceNowDelete(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Deleting PagerDuty extension %s", d.Id())
-
-	if _, err := client.Extensions.Delete(d.Id()); err != nil {
-		if perr, ok := err.(*pagerduty.Error); ok && perr.Code == 5001 {
-			log.Printf("[WARN] Extension (%s) not found, removing from state", d.Id())
-			return nil
-		}
-		return err
-	}
-
-	d.SetId("")
-
-	return nil
-}
-
-func resourcePagerDutyExtensionServiceNowImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return []*schema.ResourceData{}, err
-	}
-
-	extension, _, err := client.Extensions.Get(d.Id())
-	if err != nil {
-		return []*schema.ResourceData{}, fmt.Errorf("error importing pagerduty_extension. Expecting an importation ID for extension")
-	}
-
-	d.Set("endpoint_url", extension.EndpointURL)
-	d.Set("extension_objects", []string{extension.ExtensionObjects[0].ID})
-	d.Set("extension_schema", extension.ExtensionSchema.ID)
-
-	return []*schema.ResourceData{d}, err
-}
-
-func expandServiceNowServiceObjects(v interface{}) []*pagerduty.ServiceReference {
-	var services []*pagerduty.ServiceReference
-
-	for _, srv := range v.(*schema.Set).List() {
-		service := &pagerduty.ServiceReference{
-			Type: "service_reference",
-			ID:   srv.(string),
-		}
-		services = append(services, service)
-	}
-
-	return services
-}
-
-func flattenExtensionServiceNowObjects(serviceList []*pagerduty.ServiceReference) interface{} {
-	var services []interface{}
-	for _, s := range serviceList {
-		// only flatten service_reference types, because that's all we send at this
-		// time
-		if s.Type == "service_reference" {
-			services = append(services, s.ID)
-		}
-	}
-	return services
-}
diff --git a/pagerduty/import_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
similarity index 100%
rename from pagerduty/import_pagerduty_extension_servicenow_test.go
rename to pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go
index 1a5560132..8797dce8c 100644
--- a/pagerdutyplugin/provider.go
+++ b/pagerdutyplugin/provider.go
@@ -9,6 +9,8 @@ import (
 
 	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/path"
 	"github.com/hashicorp/terraform-plugin-framework/provider"
 	"github.com/hashicorp/terraform-plugin-framework/provider/schema"
 	"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -60,6 +62,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 &resourceExtensionServiceNow{} },
 		func() resource.Resource { return &resourceExtension{} },
 		func() resource.Resource { return &resourceTagAssignment{} },
 		func() resource.Resource { return &resourceTag{} },
@@ -188,3 +191,14 @@ type providerArguments struct {
 	ApiUrlOverride            types.String `tfsdk:"api_url_override"`
 	UseAppOauthScopedToken    types.List   `tfsdk:"use_app_oauth_scoped_token"`
 }
+
+type SchemaGetter interface {
+	GetAttribute(context.Context, path.Path, interface{}) diag.Diagnostics
+}
+
+func extractString(ctx context.Context, schema SchemaGetter, name string, diags *diag.Diagnostics) *string {
+	var s types.String
+	d := schema.GetAttribute(ctx, path.Root(name), &s)
+	diags.Append(d...)
+	return s.ValueStringPointer()
+}
diff --git a/pagerdutyplugin/resource_pagerduty_extension_servicenow.go b/pagerdutyplugin/resource_pagerduty_extension_servicenow.go
new file mode 100644
index 000000000..0586d1cd4
--- /dev/null
+++ b/pagerdutyplugin/resource_pagerduty_extension_servicenow.go
@@ -0,0 +1,357 @@
+package pagerduty
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+)
+
+type resourceExtensionServiceNow struct{ client *pagerduty.Client }
+
+var (
+	_ resource.ResourceWithConfigure   = (*resourceExtensionServiceNow)(nil)
+	_ resource.ResourceWithImportState = (*resourceExtensionServiceNow)(nil)
+)
+
+func (r *resourceExtensionServiceNow) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = "pagerduty_extension_servicenow"
+}
+
+func (r *resourceExtensionServiceNow) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id":           schema.StringAttribute{Computed: true},
+			"name":         schema.StringAttribute{Optional: true, Computed: true},
+			"html_url":     schema.StringAttribute{Computed: true},
+			"type":         schema.StringAttribute{Optional: true, Computed: true},
+			"endpoint_url": schema.StringAttribute{Optional: true, Sensitive: true},
+			"extension_objects": schema.SetAttribute{
+				Required:    true,
+				ElementType: types.StringType,
+				PlanModifiers: []planmodifier.Set{
+					setplanmodifier.RequiresReplace(),
+				},
+			},
+			"extension_schema": schema.StringAttribute{
+				Required: true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"snow_user":     schema.StringAttribute{Required: true},
+			"snow_password": schema.StringAttribute{Required: true, Sensitive: true},
+			"summary":       schema.StringAttribute{Optional: true, Computed: true},
+			"sync_options": schema.StringAttribute{
+				Required: true,
+				Validators: []validator.String{
+					stringvalidator.OneOf("manual_sync", "sync_all"),
+				},
+			},
+			"target":    schema.StringAttribute{Required: true},
+			"task_type": schema.StringAttribute{Required: true},
+			"referer":   schema.StringAttribute{Required: true},
+		},
+	}
+}
+
+func (r *resourceExtensionServiceNow) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var model resourceExtensionServiceNowModel
+
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	plan := buildPagerdutyExtensionServiceNow(ctx, &model, &resp.Diagnostics)
+	log.Printf("[INFO] Creating extension service now %s", plan.Name)
+
+	extension, err := r.client.CreateExtensionWithContext(ctx, plan)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error creating extension service now %s", plan.Name),
+			err.Error(),
+		)
+		return
+	}
+	plan.ID = extension.ID
+
+	model, err = r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
+		ID:            plan.ID,
+		RetryNotFound: false,
+		SnowPassword:  extractString(ctx, req.Plan, "snow_password", &resp.Diagnostics),
+		EndpointURL:   extractString(ctx, req.Plan, "endpoint_url", &resp.Diagnostics),
+		Diagnostics:   &resp.Diagnostics,
+	})
+	if err != nil {
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+func (r *resourceExtensionServiceNow) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state resourceExtensionServiceNowModel
+
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	log.Printf("[INFO] Reading extension service now %s", state.ID)
+	id := state.ID.ValueString()
+
+	state, err := r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
+		ID:            id,
+		RetryNotFound: false,
+		SnowPassword:  extractString(ctx, req.State, "snow_password", &resp.Diagnostics),
+		EndpointURL:   extractString(ctx, req.State, "endpoint_url", &resp.Diagnostics),
+		Diagnostics:   &resp.Diagnostics,
+	})
+	if err != nil {
+		if util.IsNotFoundError(err) {
+			resp.State.RemoveResource(ctx)
+		}
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *resourceExtensionServiceNow) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var model resourceExtensionServiceNowModel
+
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	plan := buildPagerdutyExtensionServiceNow(ctx, &model, &resp.Diagnostics)
+	if plan.ID == "" {
+		var id string
+		req.State.GetAttribute(ctx, path.Root("id"), &id)
+		plan.ID = id
+	}
+	log.Printf("[INFO] Updating extension service now %s", plan.ID)
+
+	_, err := r.client.UpdateExtensionWithContext(ctx, plan.ID, plan)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error updating extension service now %s", plan.ID),
+			err.Error(),
+		)
+		return
+	}
+
+	model, err = r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
+		ID:            plan.ID,
+		RetryNotFound: true,
+		SnowPassword:  extractString(ctx, req.State, "snow_password", &resp.Diagnostics),
+		EndpointURL:   extractString(ctx, req.State, "endpoint_url", &resp.Diagnostics),
+		Diagnostics:   &resp.Diagnostics,
+	})
+	if err != nil {
+		if util.IsNotFoundError(err) {
+			resp.State.RemoveResource(ctx)
+		}
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+func (r *resourceExtensionServiceNow) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var id types.String
+
+	resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	log.Printf("[INFO] Deleting extension service now %s", id)
+
+	err := r.client.DeleteExtensionWithContext(ctx, id.ValueString())
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error deleting extension service now %s", id),
+			err.Error(),
+		)
+		return
+	}
+	resp.State.RemoveResource(ctx)
+}
+
+func (r *resourceExtensionServiceNow) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...)
+}
+
+func (r *resourceExtensionServiceNow) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	// resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+
+	model, err := r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
+		ID:            req.ID,
+		RetryNotFound: false,
+		Diagnostics:   &resp.Diagnostics,
+	})
+	if err != nil {
+		if util.IsNotFoundError(err) {
+			resp.State.RemoveResource(ctx)
+		}
+		return
+	}
+
+	// model.EndpointURL
+	// model.ExtensionObjects = []string{extension.ExtensionObjects[0].ID}
+	// model.ExtensionSchema
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+type resourceExtensionServiceNowModel struct {
+	ID               types.String `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	HTMLURL          types.String `tfsdk:"html_url"`
+	Type             types.String `tfsdk:"type"`
+	EndpointURL      types.String `tfsdk:"endpoint_url"`
+	ExtensionObjects types.Set    `tfsdk:"extension_objects"`
+	ExtensionSchema  types.String `tfsdk:"extension_schema"`
+	SnowUser         types.String `tfsdk:"snow_user"`
+	SnowPassword     types.String `tfsdk:"snow_password"`
+	Summary          types.String `tfsdk:"summary"`
+	SyncOptions      types.String `tfsdk:"sync_options"`
+	Target           types.String `tfsdk:"target"`
+	TaskType         types.String `tfsdk:"task_type"`
+	Referer          types.String `tfsdk:"referer"`
+}
+
+type requestGetExtensionServiceNowOptions struct {
+	ID            string
+	RetryNotFound bool
+	SnowPassword  *string
+	EndpointURL   *string
+	Diagnostics   *diag.Diagnostics
+}
+
+func (r *resourceExtensionServiceNow) requestGetExtensionServiceNow(ctx context.Context, opts requestGetExtensionServiceNowOptions) (resourceExtensionServiceNowModel, error) {
+	var model resourceExtensionServiceNowModel
+
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		extensionServiceNow, err := r.client.GetExtensionWithContext(ctx, opts.ID)
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			if !opts.RetryNotFound && util.IsNotFoundError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+		model = flattenExtensionServiceNow(extensionServiceNow, opts.SnowPassword, opts.EndpointURL)
+		return nil
+	})
+	if err != nil && (opts.RetryNotFound || !util.IsNotFoundError(err)) {
+		opts.Diagnostics.AddError(
+			fmt.Sprintf("Error reading extension service now %s", opts.ID),
+			err.Error(),
+		)
+	}
+
+	return model, err
+}
+
+func buildPagerdutyExtensionServiceNow(ctx context.Context, model *resourceExtensionServiceNowModel, diags *diag.Diagnostics) *pagerduty.Extension {
+	config := &PagerDutyExtensionServiceNowConfig{
+		User:        model.SnowUser.ValueString(),
+		Password:    model.SnowPassword.ValueString(),
+		SyncOptions: model.SyncOptions.ValueString(),
+		Target:      model.Target.ValueString(),
+		TaskType:    model.TaskType.ValueString(),
+		Referer:     model.Referer.ValueString(),
+	}
+	extensionServiceNow := pagerduty.Extension{
+		APIObject: pagerduty.APIObject{
+			ID:   model.ID.ValueString(),
+			Type: "extension",
+		},
+		Name:        model.Name.ValueString(),
+		EndpointURL: model.EndpointURL.ValueString(),
+		ExtensionSchema: pagerduty.APIObject{
+			ID:   model.ExtensionSchema.ValueString(),
+			Type: "extension_schema_reference",
+		},
+		ExtensionObjects: buildExtensionServiceNowObjects(ctx, model.ExtensionObjects, diags),
+		Config:           config,
+	}
+	return &extensionServiceNow
+}
+
+func buildExtensionServiceNowObjects(ctx context.Context, set types.Set, diags *diag.Diagnostics) []pagerduty.APIObject {
+	var target []string
+	diags.Append(set.ElementsAs(ctx, &target, false)...)
+
+	list := []pagerduty.APIObject{}
+	for _, s := range target {
+		list = append(list, pagerduty.APIObject{Type: "service_reference", ID: s})
+	}
+
+	return list
+}
+
+func flattenExtensionServiceNow(src *pagerduty.Extension, snowPassword *string, endpointURL *string) resourceExtensionServiceNowModel {
+	model := resourceExtensionServiceNowModel{
+		ID:               types.StringValue(src.ID),
+		Name:             types.StringValue(src.Name),
+		EndpointURL:      types.StringValue(src.EndpointURL),
+		HTMLURL:          types.StringValue(src.HTMLURL),
+		ExtensionSchema:  types.StringValue(src.ExtensionSchema.ID),
+		ExtensionObjects: flattenExtensionServiceNowObjects(src.ExtensionObjects),
+	}
+
+	b, _ := json.Marshal(src.Config)
+	var config PagerDutyExtensionServiceNowConfig
+	json.Unmarshal(b, &config)
+
+	model.SnowUser = types.StringValue(config.User)
+	if snowPassword != nil {
+		model.SnowPassword = types.StringValue(*snowPassword)
+	}
+	if endpointURL != nil {
+		model.SnowPassword = types.StringValue(*endpointURL)
+	}
+	model.SyncOptions = types.StringValue(config.SyncOptions)
+	model.Target = types.StringValue(config.Target)
+	model.TaskType = types.StringValue(config.TaskType)
+	model.Referer = types.StringValue(config.Referer)
+	return model
+}
+
+func flattenExtensionServiceNowObjects(list []pagerduty.APIObject) types.Set {
+	elements := make([]attr.Value, 0, len(list))
+	for _, s := range list {
+		if s.Type == "service_reference" {
+			elements = append(elements, types.StringValue(s.ID))
+		}
+	}
+	return types.SetValueMust(types.StringType, elements)
+}
+
+type PagerDutyExtensionServiceNowConfig struct {
+	User        string `json:"snow_user"`
+	Password    string `json:"snow_password,omitempty"`
+	SyncOptions string `json:"sync_options"`
+	Target      string `json:"target"`
+	TaskType    string `json:"task_type"`
+	Referer     string `json:"referer"`
+}
diff --git a/pagerduty/resource_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
similarity index 100%
rename from pagerduty/resource_pagerduty_extension_servicenow_test.go
rename to pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go

From b8955e7428f53e7566e1f12ed211072be5f92b35 Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Thu, 14 Mar 2024 07:10:25 -0300
Subject: [PATCH 07/10] Migrate data source migration schema

---
 .../data_source_pagerduty_extension_schema.go | 76 ---------------
 pagerduty/provider.go                         |  1 -
 .../data_source_pagerduty_extension_schema.go | 96 +++++++++++++++++++
 ..._source_pagerduty_extension_schema_test.go | 25 ++++-
 ...ort_pagerduty_extension_servicenow_test.go |  6 +-
 pagerdutyplugin/provider.go                   |  1 +
 ...rce_pagerduty_extension_servicenow_test.go | 36 +++----
 7 files changed, 136 insertions(+), 105 deletions(-)
 delete mode 100644 pagerduty/data_source_pagerduty_extension_schema.go
 create mode 100644 pagerdutyplugin/data_source_pagerduty_extension_schema.go
 rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_extension_schema_test.go (68%)

diff --git a/pagerduty/data_source_pagerduty_extension_schema.go b/pagerduty/data_source_pagerduty_extension_schema.go
deleted file mode 100644
index 155d95442..000000000
--- a/pagerduty/data_source_pagerduty_extension_schema.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package pagerduty
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/heimweh/go-pagerduty/pagerduty"
-)
-
-func dataSourcePagerDutyExtensionSchema() *schema.Resource {
-	return &schema.Resource{
-		Read: dataSourcePagerDutyExtensionSchemaRead,
-
-		Schema: map[string]*schema.Schema{
-			"name": {
-				Type:     schema.TypeString,
-				Required: true,
-			},
-			"type": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-		},
-	}
-}
-
-func dataSourcePagerDutyExtensionSchemaRead(d *schema.ResourceData, meta interface{}) error {
-	client, err := meta.(*Config).Client()
-	if err != nil {
-		return err
-	}
-
-	log.Printf("[INFO] Reading PagerDuty Extension Schema")
-
-	searchName := d.Get("name").(string)
-
-	return retry.Retry(5*time.Minute, func() *retry.RetryError {
-		resp, _, err := client.ExtensionSchemas.List(&pagerduty.ListExtensionSchemasOptions{Query: searchName})
-		if 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)
-		}
-
-		var found *pagerduty.ExtensionSchema
-
-		for _, schema := range resp.ExtensionSchemas {
-			if strings.EqualFold(schema.Label, searchName) {
-				found = schema
-				break
-			}
-		}
-
-		if found == nil {
-			return retry.NonRetryableError(
-				fmt.Errorf("Unable to locate any extension schema with the name: %s", searchName),
-			)
-		}
-
-		d.SetId(found.ID)
-		d.Set("name", found.Label)
-		d.Set("type", found.Type)
-
-		return nil
-	})
-}
diff --git a/pagerduty/provider.go b/pagerduty/provider.go
index 410725768..d0c38576b 100644
--- a/pagerduty/provider.go
+++ b/pagerduty/provider.go
@@ -89,7 +89,6 @@ func Provider(isMux bool) *schema.Provider {
 			"pagerduty_user_contact_method":                        dataSourcePagerDutyUserContactMethod(),
 			"pagerduty_team":                                       dataSourcePagerDutyTeam(),
 			"pagerduty_vendor":                                     dataSourcePagerDutyVendor(),
-			"pagerduty_extension_schema":                           dataSourcePagerDutyExtensionSchema(),
 			"pagerduty_service":                                    dataSourcePagerDutyService(),
 			"pagerduty_service_integration":                        dataSourcePagerDutyServiceIntegration(),
 			"pagerduty_business_service":                           dataSourcePagerDutyBusinessService(),
diff --git a/pagerdutyplugin/data_source_pagerduty_extension_schema.go b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
new file mode 100644
index 000000000..74e8d27fd
--- /dev/null
+++ b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
@@ -0,0 +1,96 @@
+package pagerduty
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"strings"
+	"time"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
+)
+
+type dataSourceExtensionSchema struct{ client *pagerduty.Client }
+
+var _ datasource.DataSourceWithConfigure = (*dataSourceExtensionSchema)(nil)
+
+func (*dataSourceExtensionSchema) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = "pagerduty_extension_schema"
+}
+
+func (*dataSourceExtensionSchema) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id":   schema.StringAttribute{Computed: true},
+			"name": schema.StringAttribute{Required: true},
+			"type": schema.StringAttribute{Computed: true},
+		},
+	}
+}
+
+func (d *dataSourceExtensionSchema) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...)
+}
+
+func (d *dataSourceExtensionSchema) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	log.Println("[INFO] Reading PagerDuty extension schema")
+
+	var searchName types.String
+	resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("name"), &searchName)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var found *pagerduty.ExtensionSchema
+	// TODO delete and comment in PR: changed to 2min because 5min/30s is 10 attempts
+	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+		list, err := d.client.ListExtensionSchemasWithContext(ctx, pagerduty.ListExtensionSchemaOptions{})
+		if err != nil {
+			if util.IsBadRequestError(err) {
+				return retry.NonRetryableError(err)
+			}
+			return retry.RetryableError(err)
+		}
+
+		for _, extensionSchema := range list.ExtensionSchemas {
+			if strings.EqualFold(extensionSchema.Label, searchName.ValueString()) {
+				found = &extensionSchema
+				break
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Error reading PagerDuty extension schema %s", searchName),
+			err.Error(),
+		)
+	}
+
+	if found == nil {
+		resp.Diagnostics.AddError(
+			fmt.Sprintf("Unable to locate any extension schema with the name: %s", searchName),
+			"",
+		)
+		return
+	}
+
+	model := dataSourceExtensionSchemaModel{
+		ID:   types.StringValue(found.ID),
+		Name: types.StringValue(found.Label),
+		Type: types.StringValue(found.Type),
+	}
+	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
+}
+
+type dataSourceExtensionSchemaModel struct {
+	ID   types.String `tfsdk:"id"`
+	Name types.String `tfsdk:"name"`
+	Type types.String `tfsdk:"type"`
+}
diff --git a/pagerduty/data_source_pagerduty_extension_schema_test.go b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
similarity index 68%
rename from pagerduty/data_source_pagerduty_extension_schema_test.go
rename to pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
index c7880e087..eda6d5526 100644
--- a/pagerduty/data_source_pagerduty_extension_schema_test.go
+++ b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
@@ -1,18 +1,20 @@
 package pagerduty
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
+	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
 )
 
 func TestAccDataSourcePagerDutyExtensionSchema_Basic(t *testing.T) {
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyScheduleDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyScheduleDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccDataSourcePagerDutyExtensionSchemaConfig,
@@ -26,7 +28,6 @@ func TestAccDataSourcePagerDutyExtensionSchema_Basic(t *testing.T) {
 
 func testAccDataSourcePagerDutyExtensionSchema(n string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
-
 		r := s.RootModule().Resources[n]
 		a := r.Primary.Attributes
 
@@ -55,3 +56,19 @@ data "pagerduty_extension_schema" "foo" {
   name = "slack"
 }
 `
+
+func testAccCheckPagerDutyScheduleDestroy(s *terraform.State) error {
+	for _, r := range s.RootModule().Resources {
+		if r.Type != "pagerduty_schedule" {
+			continue
+		}
+
+		ctx := context.Background()
+		opts := pagerduty.GetScheduleOptions{}
+		if _, err := testAccProvider.client.GetScheduleWithContext(ctx, r.Primary.ID, opts); err == nil {
+			return fmt.Errorf("Schedule still exists")
+		}
+
+	}
+	return nil
+}
diff --git a/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
index c0f0d05c5..901da0751 100644
--- a/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
+++ b/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
@@ -14,9 +14,9 @@ func TestAccPagerDutyExtensionServiceNow_import(t *testing.T) {
 	url := "https://example.com/receive_a_pagerduty_webhook"
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyExtensionServiceNowDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyExtensionServiceNowDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyExtensionServiceNowConfig(name, extension_name, url, "false", "any"),
diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go
index 8797dce8c..59e3c2414 100644
--- a/pagerdutyplugin/provider.go
+++ b/pagerdutyplugin/provider.go
@@ -52,6 +52,7 @@ func (p *Provider) Schema(ctx context.Context, req provider.SchemaRequest, resp
 func (p *Provider) DataSources(ctx context.Context) [](func() datasource.DataSource) {
 	return [](func() datasource.DataSource){
 		func() datasource.DataSource { return &dataSourceBusinessService{} },
+		func() datasource.DataSource { return &dataSourceExtensionSchema{} },
 		func() datasource.DataSource { return &dataSourceStandardsResourceScores{} },
 		func() datasource.DataSource { return &dataSourceStandardsResourcesScores{} },
 		func() datasource.DataSource { return &dataSourceStandards{} },
diff --git a/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
index ed1aea18b..03afbd9c7 100644
--- a/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
+++ b/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
@@ -1,15 +1,16 @@
 package pagerduty
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"strings"
 	"testing"
 
+	"github.com/PagerDuty/go-pagerduty"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
-	"github.com/heimweh/go-pagerduty/pagerduty"
 )
 
 func init() {
@@ -19,18 +20,10 @@ func init() {
 	})
 }
 
-func testSweepExtensionServiceNow(region string) error {
-	config, err := sharedConfigForRegion(region)
-	if err != nil {
-		return err
-	}
-
-	client, err := config.Client()
-	if err != nil {
-		return err
-	}
+func testSweepExtensionServiceNow(_ string) error {
+	ctx := context.Background()
 
-	resp, _, err := client.Extensions.List(&pagerduty.ListExtensionsOptions{})
+	resp, err := testAccProvider.client.ListExtensionsWithContext(ctx, pagerduty.ListExtensionOptions{})
 	if err != nil {
 		return err
 	}
@@ -38,7 +31,7 @@ func testSweepExtensionServiceNow(region string) error {
 	for _, extension := range resp.Extensions {
 		if strings.HasPrefix(extension.Name, "test") || strings.HasPrefix(extension.Name, "tf-") {
 			log.Printf("Destroying extension %s (%s)", extension.Name, extension.ID)
-			if _, err := client.Extensions.Delete(extension.ID); err != nil {
+			if err := testAccProvider.client.DeleteExtensionWithContext(ctx, extension.ID); err != nil {
 				return err
 			}
 		}
@@ -55,9 +48,9 @@ func TestAccPagerDutyExtensionServiceNow_Basic(t *testing.T) {
 	url_updated := "https://example.com/webhook_foo"
 
 	resource.Test(t, resource.TestCase{
-		PreCheck:     func() { testAccPreCheck(t) },
-		Providers:    testAccProviders,
-		CheckDestroy: testAccCheckPagerDutyExtensionServiceNowDestroy,
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
+		CheckDestroy:             testAccCheckPagerDutyExtensionServiceNowDestroy,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccCheckPagerDutyExtensionServiceNowConfig(name, extension_name, url, "false", "any"),
@@ -116,13 +109,14 @@ func TestAccPagerDutyExtensionServiceNow_Basic(t *testing.T) {
 }
 
 func testAccCheckPagerDutyExtensionServiceNowDestroy(s *terraform.State) error {
-	client, _ := testAccProvider.Meta().(*Config).Client()
+	ctx := context.Background()
+
 	for _, r := range s.RootModule().Resources {
 		if r.Type != "pagerduty_extension_servicenow" {
 			continue
 		}
 
-		if _, _, err := client.Extensions.Get(r.Primary.ID); err == nil {
+		if _, err := testAccProvider.client.GetExtensionWithContext(ctx, r.Primary.ID); err == nil {
 			return fmt.Errorf("Extension still exists")
 		}
 
@@ -131,6 +125,8 @@ func testAccCheckPagerDutyExtensionServiceNowDestroy(s *terraform.State) error {
 }
 
 func testAccCheckPagerDutyExtensionServiceNowExists(n string) resource.TestCheckFunc {
+	ctx := context.Background()
+
 	return func(s *terraform.State) error {
 		rs, ok := s.RootModule().Resources[n]
 		if !ok {
@@ -141,9 +137,7 @@ func testAccCheckPagerDutyExtensionServiceNowExists(n string) resource.TestCheck
 			return fmt.Errorf("No extension ID is set")
 		}
 
-		client, _ := testAccProvider.Meta().(*Config).Client()
-
-		found, _, err := client.Extensions.Get(rs.Primary.ID)
+		found, err := testAccProvider.client.GetExtensionWithContext(ctx, rs.Primary.ID)
 		if err != nil {
 			return err
 		}

From a3f7d22d1a2e301624a7ac067c5095056c7d998b Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Thu, 25 Apr 2024 17:20:13 -0400
Subject: [PATCH 08/10] Fix PR comments

---
 .../data_source_pagerduty_extension_schema.go |  3 +--
 ..._source_pagerduty_extension_schema_test.go | 12 ++++-----
 ...ort_pagerduty_extension_servicenow_test.go |  8 +++---
 .../import_pagerduty_extension_test.go        |  8 +++---
 pagerdutyplugin/provider_test.go              | 10 ++++---
 .../resource_pagerduty_business_service.go    |  6 ++---
 ...resource_pagerduty_extension_servicenow.go | 19 ++++++--------
 ...rce_pagerduty_extension_servicenow_test.go |  2 +-
 .../resource_pagerduty_extension_test.go      | 17 ++++++------
 .../resource_pagerduty_tag_assignment.go      |  1 -
 util/util.go                                  | 26 ++++++++++++++++++-
 11 files changed, 67 insertions(+), 45 deletions(-)

diff --git a/pagerdutyplugin/data_source_pagerduty_extension_schema.go b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
index 74e8d27fd..6eb435177 100644
--- a/pagerdutyplugin/data_source_pagerduty_extension_schema.go
+++ b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
@@ -48,9 +48,8 @@ func (d *dataSourceExtensionSchema) Read(ctx context.Context, req datasource.Rea
 	}
 
 	var found *pagerduty.ExtensionSchema
-	// TODO delete and comment in PR: changed to 2min because 5min/30s is 10 attempts
 	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
-		list, err := d.client.ListExtensionSchemasWithContext(ctx, pagerduty.ListExtensionSchemaOptions{})
+		list, err := d.client.ListExtensionSchemasWithContext(ctx, pagerduty.ListExtensionSchemaOptions{Limit: 100})
 		if err != nil {
 			if util.IsBadRequestError(err) {
 				return retry.NonRetryableError(err)
diff --git a/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
index eda6d5526..55eaaa591 100644
--- a/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
+++ b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go
@@ -35,16 +35,16 @@ func testAccDataSourcePagerDutyExtensionSchema(n string) resource.TestCheckFunc
 			return fmt.Errorf("Expected to get an Extension Schema  ID from PagerDuty")
 		}
 
-		if a["id"] != "PD8SURB" {
-			return fmt.Errorf("Expected the Slack Extension Schema ID to be: PD8SURB, but got: %s", a["id"])
+		if a["id"] != "PAKM60Z" {
+			return fmt.Errorf("Expected Schema ID to be: PAKM60Z, but got: %s", a["id"])
 		}
 
-		if a["name"] != "Slack" {
-			return fmt.Errorf("Expected the Slack Extension Schema Name to be: Slack, but got: %s", a["name"])
+		if a["name"] != "ServiceNow (v7)" {
+			return fmt.Errorf("Expected Schema Name to be: ServiceNow (v7), but got: %s", a["name"])
 		}
 
 		if a["type"] != "extension_schema" {
-			return fmt.Errorf("Expected the Slack Extension Schema Type to be: extension_schema, but got: %s", a["type"])
+			return fmt.Errorf("Expected the Schema Type to be: extension_schema, but got: %s", a["type"])
 		}
 
 		return nil
@@ -53,7 +53,7 @@ func testAccDataSourcePagerDutyExtensionSchema(n string) resource.TestCheckFunc
 
 const testAccDataSourcePagerDutyExtensionSchemaConfig = `
 data "pagerduty_extension_schema" "foo" {
-  name = "slack"
+  name = "ServiceNow (v7)"
 }
 `
 
diff --git a/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
index 901da0751..3fa273127 100644
--- a/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
+++ b/pagerdutyplugin/import_pagerduty_extension_servicenow_test.go
@@ -21,11 +21,11 @@ func TestAccPagerDutyExtensionServiceNow_import(t *testing.T) {
 			{
 				Config: testAccCheckPagerDutyExtensionServiceNowConfig(name, extension_name, url, "false", "any"),
 			},
-
 			{
-				ResourceName:      "pagerduty_extension_servicenow.foo",
-				ImportState:       true,
-				ImportStateVerify: true,
+				ResourceName:            "pagerduty_extension_servicenow.foo",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"config"},
 			},
 		},
 	})
diff --git a/pagerdutyplugin/import_pagerduty_extension_test.go b/pagerdutyplugin/import_pagerduty_extension_test.go
index 82fe5bae8..52095a1f9 100644
--- a/pagerdutyplugin/import_pagerduty_extension_test.go
+++ b/pagerdutyplugin/import_pagerduty_extension_test.go
@@ -21,11 +21,11 @@ func TestAccPagerDutyExtension_import(t *testing.T) {
 			{
 				Config: testAccCheckPagerDutyExtensionConfig(name, extension_name, url, "false", "any"),
 			},
-
 			{
-				ResourceName:      "pagerduty_extension.foo",
-				ImportState:       true,
-				ImportStateVerify: true,
+				ResourceName:            "pagerduty_extension.foo",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"config"},
 			},
 		},
 	})
diff --git a/pagerdutyplugin/provider_test.go b/pagerdutyplugin/provider_test.go
index 23a1eab5e..ea839e2b2 100644
--- a/pagerdutyplugin/provider_test.go
+++ b/pagerdutyplugin/provider_test.go
@@ -71,10 +71,12 @@ func testAccProtoV5ProviderFactories() map[string]func() (tfprotov5.ProviderServ
 	}
 }
 
-// testAccTimeNow returns the current time in the given location.
-// The location defaults to Europe/Dublin but can be controlled by the
-// PAGERDUTY_TIME_ZONE environment variable. The location must match the
-// PagerDuty account time zone or diff issues might bubble up in tests.
+// testAccTimeNow returns the current time in the given location. The location
+// defaults to Europe/Dublin but can be controlled by the PAGERDUTY_TIME_ZONE
+// environment variable. The location must match the PagerDuty account time
+// zone or diff issues might bubble up in tests. Here is the list of allowed
+// Time Zone Identifier for PagerDuty accounts
+// https://developer.pagerduty.com/docs/1afe25e9c94cb-types#time-zone
 func testAccTimeNow() time.Time {
 	name := "Europe/Dublin"
 	if v := os.Getenv("PAGERDUTY_TIME_ZONE"); v != "" {
diff --git a/pagerdutyplugin/resource_pagerduty_business_service.go b/pagerdutyplugin/resource_pagerduty_business_service.go
index 58781947d..7328be1f4 100644
--- a/pagerdutyplugin/resource_pagerduty_business_service.go
+++ b/pagerdutyplugin/resource_pagerduty_business_service.go
@@ -69,7 +69,7 @@ func (r *resourceBusinessService) Create(ctx context.Context, req resource.Creat
 	if resp.Diagnostics.HasError() {
 		return
 	}
-	businessServicePlan := buildPagerdutyBusinessService(ctx, &plan)
+	businessServicePlan := buildPagerdutyBusinessService(&plan)
 	log.Printf("[INFO] Creating PagerDuty business service %s", plan.Name)
 
 	err := helperResource.RetryContext(ctx, 5*time.Minute, func() *helperResource.RetryError {
@@ -120,7 +120,7 @@ func (r *resourceBusinessService) Update(ctx context.Context, req resource.Updat
 		return
 	}
 
-	businessServicePlan := buildPagerdutyBusinessService(ctx, &plan)
+	businessServicePlan := buildPagerdutyBusinessService(&plan)
 	if businessServicePlan.ID == "" {
 		var id string
 		req.State.GetAttribute(ctx, path.Root("id"), &id)
@@ -202,7 +202,7 @@ func requestGetBusinessService(ctx context.Context, client *pagerduty.Client, id
 	return model
 }
 
-func buildPagerdutyBusinessService(_ context.Context, model *resourceBusinessServiceModel) *pagerduty.BusinessService {
+func buildPagerdutyBusinessService(model *resourceBusinessServiceModel) *pagerduty.BusinessService {
 	businessService := pagerduty.BusinessService{
 		ID:             model.ID.ValueString(),
 		Description:    model.Description.ValueString(),
diff --git a/pagerdutyplugin/resource_pagerduty_extension_servicenow.go b/pagerdutyplugin/resource_pagerduty_extension_servicenow.go
index 0586d1cd4..7abf7e4e8 100644
--- a/pagerdutyplugin/resource_pagerduty_extension_servicenow.go
+++ b/pagerdutyplugin/resource_pagerduty_extension_servicenow.go
@@ -160,8 +160,8 @@ func (r *resourceExtensionServiceNow) Update(ctx context.Context, req resource.U
 	model, err = r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
 		ID:            plan.ID,
 		RetryNotFound: true,
-		SnowPassword:  extractString(ctx, req.State, "snow_password", &resp.Diagnostics),
-		EndpointURL:   extractString(ctx, req.State, "endpoint_url", &resp.Diagnostics),
+		SnowPassword:  extractString(ctx, req.Plan, "snow_password", &resp.Diagnostics),
+		EndpointURL:   extractString(ctx, req.Plan, "endpoint_url", &resp.Diagnostics),
 		Diagnostics:   &resp.Diagnostics,
 	})
 	if err != nil {
@@ -199,8 +199,6 @@ func (r *resourceExtensionServiceNow) Configure(ctx context.Context, req resourc
 }
 
 func (r *resourceExtensionServiceNow) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
-	// resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
-
 	model, err := r.requestGetExtensionServiceNow(ctx, requestGetExtensionServiceNowOptions{
 		ID:            req.ID,
 		RetryNotFound: false,
@@ -212,10 +210,6 @@ func (r *resourceExtensionServiceNow) ImportState(ctx context.Context, req resou
 		}
 		return
 	}
-
-	// model.EndpointURL
-	// model.ExtensionObjects = []string{extension.ExtensionObjects[0].ID}
-	// model.ExtensionSchema
 	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
 }
 
@@ -313,7 +307,6 @@ func flattenExtensionServiceNow(src *pagerduty.Extension, snowPassword *string,
 	model := resourceExtensionServiceNowModel{
 		ID:               types.StringValue(src.ID),
 		Name:             types.StringValue(src.Name),
-		EndpointURL:      types.StringValue(src.EndpointURL),
 		HTMLURL:          types.StringValue(src.HTMLURL),
 		ExtensionSchema:  types.StringValue(src.ExtensionSchema.ID),
 		ExtensionObjects: flattenExtensionServiceNowObjects(src.ExtensionObjects),
@@ -321,14 +314,18 @@ func flattenExtensionServiceNow(src *pagerduty.Extension, snowPassword *string,
 
 	b, _ := json.Marshal(src.Config)
 	var config PagerDutyExtensionServiceNowConfig
-	json.Unmarshal(b, &config)
+	_ = json.Unmarshal(b, &config)
 
 	model.SnowUser = types.StringValue(config.User)
 	if snowPassword != nil {
 		model.SnowPassword = types.StringValue(*snowPassword)
+	} else if config.Password != "" {
+		model.SnowPassword = types.StringValue(config.Password)
 	}
 	if endpointURL != nil {
-		model.SnowPassword = types.StringValue(*endpointURL)
+		model.EndpointURL = types.StringValue(*endpointURL)
+	} else if src.EndpointURL != "" {
+		model.EndpointURL = types.StringValue(src.EndpointURL)
 	}
 	model.SyncOptions = types.StringValue(config.SyncOptions)
 	model.Target = types.StringValue(config.Target)
diff --git a/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go b/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
index 03afbd9c7..9dbe6e0cd 100644
--- a/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
+++ b/pagerdutyplugin/resource_pagerduty_extension_servicenow_test.go
@@ -44,7 +44,7 @@ func TestAccPagerDutyExtensionServiceNow_Basic(t *testing.T) {
 	extension_name := id.PrefixedUniqueId("tf-")
 	extension_name_updated := id.PrefixedUniqueId("tf-")
 	name := id.PrefixedUniqueId("tf-")
-	url := "https://example.com/recieve_a_pagerduty_webhook"
+	url := "https://example.com/receive_a_pagerduty_webhook"
 	url_updated := "https://example.com/webhook_foo"
 
 	resource.Test(t, resource.TestCase{
diff --git a/pagerdutyplugin/resource_pagerduty_extension_test.go b/pagerdutyplugin/resource_pagerduty_extension_test.go
index 0ce11de0b..c63934d5d 100644
--- a/pagerdutyplugin/resource_pagerduty_extension_test.go
+++ b/pagerdutyplugin/resource_pagerduty_extension_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/PagerDuty/go-pagerduty"
+	"github.com/PagerDuty/terraform-provider-pagerduty/util"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 	"github.com/hashicorp/terraform-plugin-testing/terraform"
@@ -44,7 +45,7 @@ func TestAccPagerDutyExtension_Basic(t *testing.T) {
 	extension_name := id.PrefixedUniqueId("tf-")
 	extension_name_updated := id.PrefixedUniqueId("tf-")
 	name := id.PrefixedUniqueId("tf-")
-	url := "https://example.com/recieve_a_pagerduty_webhook"
+	url := "https://example.com/receive_a_pagerduty_webhook"
 	url_updated := "https://example.com/webhook_foo"
 
 	resource.Test(t, resource.TestCase{
@@ -62,8 +63,8 @@ func TestAccPagerDutyExtension_Basic(t *testing.T) {
 						"pagerduty_extension.foo", "extension_schema", "PJFWPEP"),
 					resource.TestCheckResourceAttr(
 						"pagerduty_extension.foo", "endpoint_url", url),
-					resource.TestCheckResourceAttr(
-						"pagerduty_extension.foo", "config", "{\"notify_types\":{\"acknowledge\":false,\"assignments\":false,\"resolve\":false},\"restrict\":\"any\"}"),
+					resource.TestCheckResourceAttrWith(
+						"pagerduty_extension.foo", "config", util.CheckJSONEqual("{\"notify_types\":{\"acknowledge\":false,\"assignments\":false,\"resolve\":false},\"restrict\":\"any\"}")),
 					resource.TestCheckResourceAttr(
 						"pagerduty_extension.foo", "html_url", ""),
 				),
@@ -78,8 +79,8 @@ func TestAccPagerDutyExtension_Basic(t *testing.T) {
 						"pagerduty_extension.foo", "extension_schema", "PJFWPEP"),
 					resource.TestCheckResourceAttr(
 						"pagerduty_extension.foo", "endpoint_url", url_updated),
-					resource.TestCheckResourceAttr(
-						"pagerduty_extension.foo", "config", "{\"notify_types\":{\"acknowledge\":true,\"assignments\":true,\"resolve\":true},\"restrict\":\"pd-users\"}"),
+					resource.TestCheckResourceAttrWith(
+						"pagerduty_extension.foo", "config", util.CheckJSONEqual("{\"notify_types\":{\"acknowledge\":true,\"assignments\":true,\"resolve\":true},\"restrict\":\"pd-users\"}")),
 				),
 			},
 		},
@@ -178,9 +179,9 @@ resource "pagerduty_extension" "foo"{
 {
 	"restrict": "%[4]v",
 	"notify_types": {
-			"resolve": %[5]v,
-			"acknowledge": %[5]v,
-			"assignments": %[5]v
+		"resolve": %[5]v,
+		"acknowledge": %[5]v,
+		"assignments": %[5]v
 	}
 }
 EOF
diff --git a/pagerdutyplugin/resource_pagerduty_tag_assignment.go b/pagerdutyplugin/resource_pagerduty_tag_assignment.go
index e886d48fd..0acc15380 100644
--- a/pagerdutyplugin/resource_pagerduty_tag_assignment.go
+++ b/pagerdutyplugin/resource_pagerduty_tag_assignment.go
@@ -245,7 +245,6 @@ func (r *resourceTagAssignment) Configure(ctx context.Context, req resource.Conf
 }
 
 func (r *resourceTagAssignment) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
-	// resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
 	ids := strings.Split(req.ID, ".")
 	if len(ids) != 3 {
 		resp.Diagnostics.AddError(
diff --git a/util/util.go b/util/util.go
index ca6013509..72e91798d 100644
--- a/util/util.go
+++ b/util/util.go
@@ -15,6 +15,7 @@ import (
 	"github.com/hashicorp/go-cty/cty"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
 )
 
 func TimeToUTC(v string) (time.Time, error) {
@@ -76,7 +77,7 @@ func SuppressRFC3339Diff(k, oldTime, newTime string, d *schema.ResourceData) boo
 func SuppressScheduleLayerStartDiff(k, oldTime, newTime string, d *schema.ResourceData) bool {
 	oldT, newT, err := ParseRFC3339Time(k, oldTime, newTime)
 	if err != nil {
-		log.Printf(err.Error())
+		log.Print(err.Error())
 		return false
 	}
 
@@ -468,3 +469,26 @@ var validTZ []string = []string{
 	"Pacific/Port_Moresby",
 	"Pacific/Tongatapu",
 }
+
+// CheckJSONEqual returns a function that can be used as in input for
+// `resource.TestCheckResourceAttrWith`, it compares two json strings are
+// equivalent in data.
+func CheckJSONEqual(expected string) resource.CheckResourceAttrWithFunc {
+	return resource.CheckResourceAttrWithFunc(func(value string) error {
+		var exp interface{}
+		if err := json.Unmarshal([]byte(expected), &exp); err != nil {
+			return err
+		}
+
+		var got interface{}
+		if err := json.Unmarshal([]byte(value), &got); err != nil {
+			return err
+		}
+
+		if !reflect.DeepEqual(exp, got) {
+			return fmt.Errorf(`Received value "%v", but expected "%v"`, got, exp)
+		}
+
+		return nil
+	})
+}

From 3f7de693fdda371c5f7b1e4bbddfd77da5451998 Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Fri, 26 Apr 2024 12:06:53 -0400
Subject: [PATCH 09/10] Add pagination to extension schema

---
 .../data_source_pagerduty_extension_schema.go | 45 +++++++++++--------
 1 file changed, 27 insertions(+), 18 deletions(-)

diff --git a/pagerdutyplugin/data_source_pagerduty_extension_schema.go b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
index 6eb435177..3f0db9197 100644
--- a/pagerdutyplugin/data_source_pagerduty_extension_schema.go
+++ b/pagerdutyplugin/data_source_pagerduty_extension_schema.go
@@ -48,28 +48,37 @@ func (d *dataSourceExtensionSchema) Read(ctx context.Context, req datasource.Rea
 	}
 
 	var found *pagerduty.ExtensionSchema
-	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
-		list, err := d.client.ListExtensionSchemasWithContext(ctx, pagerduty.ListExtensionSchemaOptions{Limit: 100})
-		if err != nil {
-			if util.IsBadRequestError(err) {
-				return retry.NonRetryableError(err)
+	offset := 0
+	more := true
+	for more {
+		err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
+			o := pagerduty.ListExtensionSchemaOptions{Limit: 20, Offset: uint(offset), Total: true}
+			list, err := d.client.ListExtensionSchemasWithContext(ctx, o)
+			if err != nil {
+				if util.IsBadRequestError(err) {
+					return retry.NonRetryableError(err)
+				}
+				return retry.RetryableError(err)
 			}
-			return retry.RetryableError(err)
-		}
 
-		for _, extensionSchema := range list.ExtensionSchemas {
-			if strings.EqualFold(extensionSchema.Label, searchName.ValueString()) {
-				found = &extensionSchema
-				break
+			for _, extensionSchema := range list.ExtensionSchemas {
+				if strings.EqualFold(extensionSchema.Label, searchName.ValueString()) {
+					found = &extensionSchema
+					more = false
+					return nil
+				}
 			}
+
+			more = list.More
+			offset += len(list.ExtensionSchemas)
+			return nil
+		})
+		if err != nil {
+			resp.Diagnostics.AddError(
+				fmt.Sprintf("Error reading PagerDuty extension schema %s", searchName),
+				err.Error(),
+			)
 		}
-		return nil
-	})
-	if err != nil {
-		resp.Diagnostics.AddError(
-			fmt.Sprintf("Error reading PagerDuty extension schema %s", searchName),
-			err.Error(),
-		)
 	}
 
 	if found == nil {

From 40ea47ab32cfde92342be8943f369ba0ed1c380d Mon Sep 17 00:00:00 2001
From: Carlos Gajardo <cjavier@pagerduty.com>
Date: Fri, 26 Apr 2024 12:48:32 -0400
Subject: [PATCH 10/10] Add ctx to data tags read call

---
 pagerdutyplugin/data_source_pagerduty_tag.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pagerdutyplugin/data_source_pagerduty_tag.go b/pagerdutyplugin/data_source_pagerduty_tag.go
index 9d0e4d173..f162bb49c 100644
--- a/pagerdutyplugin/data_source_pagerduty_tag.go
+++ b/pagerdutyplugin/data_source_pagerduty_tag.go
@@ -52,14 +52,14 @@ func (d *dataSourceTag) Read(ctx context.Context, req datasource.ReadRequest, re
 
 	var tags []*pagerduty.Tag
 	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
-		list, err := d.client.ListTags(pagerduty.ListTagOptions{Query: searchTag})
+		list, err := d.client.ListTagsPaginated(ctx, pagerduty.ListTagOptions{Query: searchTag, Limit: 100})
 		if err != nil {
 			if util.IsBadRequestError(err) {
 				return retry.NonRetryableError(err)
 			}
 			return retry.RetryableError(err)
 		}
-		tags = list.Tags
+		tags = list
 		return nil
 	})
 	if err != nil {