From 9e552743c8739b9c50186a08cf898dd404661a7f Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Tue, 19 Mar 2024 22:24:21 -0300 Subject: [PATCH 1/8] Migrate data source vendor --- pagerduty/data_source_pagerduty_vendor.go | 1 + pagerduty/provider.go | 1 + ..._source_pagerduty_extension_schema_test.go | 9 +- .../data_source_pagerduty_vendor.go | 108 ++++++++++++++++++ .../data_source_pagerduty_vendor_test.go | 24 ++-- pagerdutyplugin/provider.go | 3 +- 6 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 pagerdutyplugin/data_source_pagerduty_vendor.go rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_vendor_test.go (69%) diff --git a/pagerduty/data_source_pagerduty_vendor.go b/pagerduty/data_source_pagerduty_vendor.go index 3c929796c..a1be9a69e 100644 --- a/pagerduty/data_source_pagerduty_vendor.go +++ b/pagerduty/data_source_pagerduty_vendor.go @@ -13,6 +13,7 @@ import ( "github.com/heimweh/go-pagerduty/pagerduty" ) +// Deprecated: Migrated to pagerdutyplugin.resourceVendor. Kept for testing purposes. func dataSourcePagerDutyVendor() *schema.Resource { return &schema.Resource{ Read: dataSourcePagerDutyVendorRead, diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 7de7b701f..51a7ce3e4 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -158,6 +158,7 @@ func Provider(isMux bool) *schema.Provider { delete(p.DataSourcesMap, "pagerduty_priority") delete(p.DataSourcesMap, "pagerduty_service") delete(p.DataSourcesMap, "pagerduty_service_integration") + delete(p.DataSourcesMap, "pagerduty_vendor") delete(p.ResourcesMap, "pagerduty_addon") delete(p.ResourcesMap, "pagerduty_business_service") diff --git a/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go index 55eaaa591..fc94b9fd4 100644 --- a/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go +++ b/pagerdutyplugin/data_source_pagerduty_extension_schema_test.go @@ -58,17 +58,18 @@ data "pagerduty_extension_schema" "foo" { ` func testAccCheckPagerDutyScheduleDestroy(s *terraform.State) error { + ctx := context.Background() + 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 { + _, err := testAccProvider.client.GetScheduleWithContext(ctx, r.Primary.ID, pagerduty.GetScheduleOptions{}) + if err == nil { return fmt.Errorf("Schedule still exists") } - } + return nil } diff --git a/pagerdutyplugin/data_source_pagerduty_vendor.go b/pagerdutyplugin/data_source_pagerduty_vendor.go new file mode 100644 index 000000000..5f7346c9c --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_vendor.go @@ -0,0 +1,108 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "regexp" + "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 dataSourceVendor struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceVendor)(nil) + +func (*dataSourceVendor) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_vendor" +} + +func (*dataSourceVendor) 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 *dataSourceVendor) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceVendor) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty vendor") + + var searchName types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("name"), &searchName)...) + if resp.Diagnostics.HasError() { + return + } + + var found *pagerduty.Vendor + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + list, err := d.client.ListVendorsWithContext(ctx, pagerduty.ListVendorOptions{}) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + + for _, vendor := range list.Vendors { + if strings.EqualFold(vendor.Name, searchName.ValueString()) { + found = &vendor + break + } + } + + // We didn't find an exact match, so let's fallback to partial matching. + if found == nil { + pr := regexp.MustCompile("(?i)" + searchName.ValueString()) + for _, vendor := range list.Vendors { + if pr.MatchString(vendor.Name) { + found = &vendor + break + } + } + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty vendor %s", searchName), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any vendor with the name: %s", searchName), + "", + ) + return + } + + model := dataSourceVendorModel{ + ID: types.StringValue(found.ID), + Name: types.StringValue(found.Name), + Type: types.StringValue(found.GenericServiceType), + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceVendorModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` +} diff --git a/pagerduty/data_source_pagerduty_vendor_test.go b/pagerdutyplugin/data_source_pagerduty_vendor_test.go similarity index 69% rename from pagerduty/data_source_pagerduty_vendor_test.go rename to pagerdutyplugin/data_source_pagerduty_vendor_test.go index ed3f1df4f..2cde72c2d 100644 --- a/pagerduty/data_source_pagerduty_vendor_test.go +++ b/pagerdutyplugin/data_source_pagerduty_vendor_test.go @@ -9,9 +9,9 @@ import ( func TestAccDataSourcePagerDutyVendor_Basic(t *testing.T) { dataSourceName := "data.pagerduty_vendor.foo" 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: testAccDataSourcePagerDutyVendorConfig, @@ -27,15 +27,15 @@ func TestAccDataSourcePagerDutyVendor_Basic(t *testing.T) { func TestAccDataSourcePagerDutyVendor_ExactMatch(t *testing.T) { dataSourceName := "data.pagerduty_vendor.foo" 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: testAccDataSourcePagerDutyExactMatchConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceName, "id", "PKAPG94"), - resource.TestCheckResourceAttr(dataSourceName, "name", "Sentry"), + resource.TestCheckResourceAttr(dataSourceName, "id", "PAM4FGS"), + resource.TestCheckResourceAttr(dataSourceName, "name", "Datadog"), ), }, }, @@ -45,9 +45,9 @@ func TestAccDataSourcePagerDutyVendor_ExactMatch(t *testing.T) { func TestAccDataSourcePagerDutyVendor_SpecialChars(t *testing.T) { dataSourceName := "data.pagerduty_vendor.foo" 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: testAccDataSourcePagerDutySpecialCharsConfig, @@ -68,7 +68,7 @@ data "pagerduty_vendor" "foo" { const testAccDataSourcePagerDutyExactMatchConfig = ` data "pagerduty_vendor" "foo" { - name = "sentry" + name = "datadog" } ` diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 4a6f7e795..9ea5337be 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -55,14 +55,15 @@ func (p *Provider) DataSources(_ context.Context) [](func() datasource.DataSourc func() datasource.DataSource { return &dataSourceBusinessService{} }, func() datasource.DataSource { return &dataSourceExtensionSchema{} }, func() datasource.DataSource { return &dataSourceIntegration{} }, - func() datasource.DataSource { return &dataSourceLicense{} }, func() datasource.DataSource { return &dataSourceLicenses{} }, + func() datasource.DataSource { return &dataSourceLicense{} }, func() datasource.DataSource { return &dataSourcePriority{} }, func() datasource.DataSource { return &dataSourceService{} }, func() datasource.DataSource { return &dataSourceStandardsResourceScores{} }, func() datasource.DataSource { return &dataSourceStandardsResourcesScores{} }, func() datasource.DataSource { return &dataSourceStandards{} }, func() datasource.DataSource { return &dataSourceTag{} }, + func() datasource.DataSource { return &dataSourceVendor{} }, } } From 3e9b83b7bb7331dbee65b7750589a878500b609d Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Wed, 20 Mar 2024 00:55:48 -0300 Subject: [PATCH 2/8] Migrate data source users --- pagerduty/data_source_pagerduty_users.go | 122 ----------------- pagerduty/provider.go | 1 - .../data_source_pagerduty_users.go | 129 ++++++++++++++++++ .../data_source_pagerduty_users_test.go | 22 ++- pagerdutyplugin/provider.go | 1 + pagerdutyplugin/provider_test.go | 4 + 6 files changed, 151 insertions(+), 128 deletions(-) delete mode 100644 pagerduty/data_source_pagerduty_users.go create mode 100644 pagerdutyplugin/data_source_pagerduty_users.go rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_users_test.go (91%) diff --git a/pagerduty/data_source_pagerduty_users.go b/pagerduty/data_source_pagerduty_users.go deleted file mode 100644 index f5f79e2e1..000000000 --- a/pagerduty/data_source_pagerduty_users.go +++ /dev/null @@ -1,122 +0,0 @@ -package pagerduty - -import ( - "log" - "net/http" - "strconv" - "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 dataSourcePagerDutyUsers() *schema.Resource { - return &schema.Resource{ - Read: dataSourcePagerDutyUsersRead, - - Schema: map[string]*schema.Schema{ - "team_ids": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "users": { - Type: schema.TypeList, - Computed: true, - Description: "List of users who are members of the team", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - }, - "name": { - Type: schema.TypeString, - Computed: true, - }, - "email": { - Type: schema.TypeString, - Computed: true, - }, - "role": { - Type: schema.TypeString, - Computed: true, - }, - "type": { - Type: schema.TypeString, - Computed: true, - }, - "job_title": { - Type: schema.TypeString, - Computed: true, - }, - "time_zone": { - Type: schema.TypeString, - Computed: true, - }, - "description": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func dataSourcePagerDutyUsersRead(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - log.Printf("[INFO] Reading PagerDuty users") - - pre := d.Get("team_ids").([]interface{}) - var teamIds []string - for _, ti := range pre { - teamIds = append(teamIds, ti.(string)) - } - - o := &pagerduty.ListUsersOptions{ - TeamIDs: teamIds, - } - - return retry.Retry(5*time.Minute, func() *retry.RetryError { - resp, err := client.Users.ListAll(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 users []map[string]interface{} - for _, user := range resp { - users = append(users, map[string]interface{}{ - "id": user.ID, - "name": user.Name, - "email": user.Email, - "role": user.Role, - "job_title": user.JobTitle, - "time_zone": user.TimeZone, - "description": user.Description, - }) - } - - // Since this data doesn't have an unique ID, this force this data to be - // refreshed in every Terraform apply - d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) - d.Set("users", users) - - return nil - }) -} diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 51a7ce3e4..1dbccc6ad 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -89,7 +89,6 @@ func Provider(isMux bool) *schema.Provider { "pagerduty_escalation_policy": dataSourcePagerDutyEscalationPolicy(), "pagerduty_schedule": dataSourcePagerDutySchedule(), "pagerduty_user": dataSourcePagerDutyUser(), - "pagerduty_users": dataSourcePagerDutyUsers(), "pagerduty_licenses": dataSourcePagerDutyLicenses(), "pagerduty_user_contact_method": dataSourcePagerDutyUserContactMethod(), "pagerduty_team": dataSourcePagerDutyTeam(), diff --git a/pagerdutyplugin/data_source_pagerduty_users.go b/pagerdutyplugin/data_source_pagerduty_users.go new file mode 100644 index 000000000..149243be5 --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_users.go @@ -0,0 +1,129 @@ +package pagerduty + +import ( + "context" + "log" + "strconv" + "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/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 dataSourceUsers struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceUsers)(nil) + +func (*dataSourceUsers) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_users" +} + +func (*dataSourceUsers) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "team_ids": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + "users": schema.ListAttribute{ + Computed: true, + Description: "List of users who are members of the team", + ElementType: userObjectType, + }, + }, + } +} + +func (d *dataSourceUsers) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceUsers) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty users") + + var list types.List + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("team_ids"), &list)...) + if resp.Diagnostics.HasError() { + return + } + + var teamIdStrings []types.String + resp.Diagnostics.Append(list.ElementsAs(ctx, &teamIdStrings, false)...) + if resp.Diagnostics.HasError() { + return + } + + teamIds := make([]string, 0, len(list.Elements())) + for _, v := range teamIdStrings { + teamIds = append(teamIds, v.ValueString()) + } + opts := pagerduty.ListUsersOptions{TeamIDs: teamIds} + + var model dataSourceUsersModel + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := d.client.ListUsersWithContext(ctx, opts) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + model = flattenUsers(response.Users, list) + return nil + }) + if err != nil { + resp.Diagnostics.AddError("Error reading PagerDuty users", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceUsersModel struct { + ID types.String `tfsdk:"id"` + Users types.List `tfsdk:"users"` + TeamIDs types.List `tfsdk:"team_ids"` +} + +var userObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "description": types.StringType, + "email": types.StringType, + "job_title": types.StringType, + "name": types.StringType, + "role": types.StringType, + "time_zone": types.StringType, + "type": types.StringType, + }, +} + +func flattenUsers(list []pagerduty.User, teamIds types.List) dataSourceUsersModel { + userValues := make([]attr.Value, 0, len(list)) + for _, u := range list { + obj := types.ObjectValueMust(userObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(u.ID), + "name": types.StringValue(u.Name), + "email": types.StringValue(u.Email), + "role": types.StringValue(u.Role), + "job_title": types.StringValue(u.JobTitle), + "time_zone": types.StringValue(u.Timezone), + "description": types.StringValue(u.Description), + "type": types.StringNull(), + }) + userValues = append(userValues, obj) + } + return dataSourceUsersModel{ + ID: types.StringValue(strconv.FormatInt(time.Now().Unix(), 10)), + Users: types.ListValueMust(userObjectType, userValues), + TeamIDs: teamIds, + } +} diff --git a/pagerduty/data_source_pagerduty_users_test.go b/pagerdutyplugin/data_source_pagerduty_users_test.go similarity index 91% rename from pagerduty/data_source_pagerduty_users_test.go rename to pagerdutyplugin/data_source_pagerduty_users_test.go index b0f5c9cc1..cc25e0141 100644 --- a/pagerduty/data_source_pagerduty_users_test.go +++ b/pagerdutyplugin/data_source_pagerduty_users_test.go @@ -33,8 +33,8 @@ func TestAccDataSourcePagerDutyUsers_Basic(t *testing.T) { timeZone3 := timeZone resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), Steps: []resource.TestStep{ { Config: testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, email1, title1, timeZone1, description1, username2, email2, title2, timeZone2, description2, username3, email3, title3, timeZone3, description3), @@ -129,10 +129,14 @@ func testAccDataSourcePagerDutyUsersExists(n string) resource.TestCheckFunc { func testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, email1, title1, timeZone1, description1, username2, email2, title2, timeZone2, description2, username3, email3, title3, timeZone3, description3 string) string { return fmt.Sprintf(` resource "pagerduty_team" "test1" { - name = "%s" + name = "%s" } resource "pagerduty_team" "test2" { - name = "%s" + name = "%s" + } + + data "pagerduty_license" "stakeholder" { + name = "Digital Operations (Stakeholder)" } resource "pagerduty_user" "test_wo_team" { @@ -141,6 +145,7 @@ func testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, emai job_title = "%s" time_zone = "%s" description = "%s" + license = data.pagerduty_license.stakeholder.id } resource "pagerduty_user" "test_w_team1" { name = "%s" @@ -148,6 +153,7 @@ func testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, emai job_title = "%s" time_zone = "%s" description = "%s" + license = data.pagerduty_license.stakeholder.id } resource "pagerduty_user" "test_w_team2" { name = "%s" @@ -155,6 +161,7 @@ func testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, emai job_title = "%s" time_zone = "%s" description = "%s" + license = data.pagerduty_license.stakeholder.id } resource "pagerduty_team_membership" "test1" { @@ -179,5 +186,10 @@ func testAccDataSourcePagerDutyUsersConfig(teamname1, teamname2, username1, emai depends_on = [pagerduty_team_membership.test2] team_ids = [pagerduty_team.test1.id, pagerduty_team.test2.id] } -`, teamname1, teamname2, username1, email1, title1, timeZone1, description1, username2, email2, title2, timeZone2, description2, username3, email3, title3, timeZone3, description3) +`, + teamname1, teamname2, + username1, email1, title1, timeZone1, description1, + username2, email2, title2, timeZone2, description2, + username3, email3, title3, timeZone3, description3, + ) } diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 9ea5337be..738796ba1 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -63,6 +63,7 @@ func (p *Provider) DataSources(_ context.Context) [](func() datasource.DataSourc func() datasource.DataSource { return &dataSourceStandardsResourcesScores{} }, func() datasource.DataSource { return &dataSourceStandards{} }, func() datasource.DataSource { return &dataSourceTag{} }, + func() datasource.DataSource { return &dataSourceUsers{} }, func() datasource.DataSource { return &dataSourceVendor{} }, } } diff --git a/pagerdutyplugin/provider_test.go b/pagerdutyplugin/provider_test.go index f562e13bb..de285753d 100644 --- a/pagerdutyplugin/provider_test.go +++ b/pagerdutyplugin/provider_test.go @@ -15,6 +15,10 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) +func TestMain(m *testing.M) { + resource.TestMain(m) +} + var testAccProvider = New() func testAccPreCheck(t *testing.T) { From 0e9ec17dbabda0486f222f6bdb6cdc904f1e3a81 Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Wed, 20 Mar 2024 17:13:19 -0300 Subject: [PATCH 3/8] Migrate data source user --- pagerduty/data_source_pagerduty_user.go | 99 ---------------- pagerduty/provider.go | 15 ++- pagerdutyplugin/data_source_pagerduty_user.go | 109 ++++++++++++++++++ .../data_source_pagerduty_user_test.go | 4 +- pagerdutyplugin/provider.go | 1 + 5 files changed, 119 insertions(+), 109 deletions(-) delete mode 100644 pagerduty/data_source_pagerduty_user.go create mode 100644 pagerdutyplugin/data_source_pagerduty_user.go rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_user_test.go (94%) diff --git a/pagerduty/data_source_pagerduty_user.go b/pagerduty/data_source_pagerduty_user.go deleted file mode 100644 index 9d9211cb8..000000000 --- a/pagerduty/data_source_pagerduty_user.go +++ /dev/null @@ -1,99 +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 dataSourcePagerDutyUser() *schema.Resource { - return &schema.Resource{ - Read: dataSourcePagerDutyUserRead, - - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Computed: true, - }, - "email": { - Type: schema.TypeString, - Required: true, - }, - "role": { - Type: schema.TypeString, - Computed: true, - }, - "job_title": { - Type: schema.TypeString, - Computed: true, - }, - "time_zone": { - Type: schema.TypeString, - Computed: true, - }, - "description": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func dataSourcePagerDutyUserRead(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - log.Printf("[INFO] Reading PagerDuty user") - - searchEmail := d.Get("email").(string) - - o := &pagerduty.ListUsersOptions{ - Query: searchEmail, - } - - return retry.Retry(5*time.Minute, func() *retry.RetryError { - resp, err := client.Users.ListAll(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.FullUser - - for _, user := range resp { - if user.Email == searchEmail { - found = user - break - } - } - - if found == nil { - return retry.NonRetryableError( - fmt.Errorf("Unable to locate any user with the email: %s", searchEmail), - ) - } - - d.SetId(found.ID) - d.Set("name", found.Name) - d.Set("email", found.Email) - d.Set("role", found.Role) - d.Set("job_title", found.JobTitle) - d.Set("time_zone", found.TimeZone) - d.Set("description", found.Description) - - return nil - }) -} diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 1dbccc6ad..ee7b8cdd5 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -88,25 +88,24 @@ func Provider(isMux bool) *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ "pagerduty_escalation_policy": dataSourcePagerDutyEscalationPolicy(), "pagerduty_schedule": dataSourcePagerDutySchedule(), - "pagerduty_user": dataSourcePagerDutyUser(), "pagerduty_licenses": dataSourcePagerDutyLicenses(), "pagerduty_user_contact_method": dataSourcePagerDutyUserContactMethod(), "pagerduty_team": dataSourcePagerDutyTeam(), "pagerduty_vendor": dataSourcePagerDutyVendor(), "pagerduty_service": dataSourcePagerDutyService(), "pagerduty_service_integration": dataSourcePagerDutyServiceIntegration(), + "pagerduty_automation_actions_action": dataSourcePagerDutyAutomationActionsAction(), + "pagerduty_automation_actions_runner": dataSourcePagerDutyAutomationActionsRunner(), "pagerduty_business_service": dataSourcePagerDutyBusinessService(), - "pagerduty_priority": dataSourcePagerDutyPriority(), - "pagerduty_ruleset": dataSourcePagerDutyRuleset(), "pagerduty_event_orchestration": dataSourcePagerDutyEventOrchestration(), - "pagerduty_event_orchestrations": dataSourcePagerDutyEventOrchestrations(), - "pagerduty_event_orchestration_integration": dataSourcePagerDutyEventOrchestrationIntegration(), "pagerduty_event_orchestration_global_cache_variable": dataSourcePagerDutyEventOrchestrationGlobalCacheVariable(), + "pagerduty_event_orchestration_integration": dataSourcePagerDutyEventOrchestrationIntegration(), "pagerduty_event_orchestration_service_cache_variable": dataSourcePagerDutyEventOrchestrationServiceCacheVariable(), - "pagerduty_automation_actions_runner": dataSourcePagerDutyAutomationActionsRunner(), - "pagerduty_automation_actions_action": dataSourcePagerDutyAutomationActionsAction(), - "pagerduty_incident_workflow": dataSourcePagerDutyIncidentWorkflow(), + "pagerduty_event_orchestrations": dataSourcePagerDutyEventOrchestrations(), "pagerduty_incident_custom_field": dataSourcePagerDutyIncidentCustomField(), + "pagerduty_incident_workflow": dataSourcePagerDutyIncidentWorkflow(), + "pagerduty_priority": dataSourcePagerDutyPriority(), + "pagerduty_ruleset": dataSourcePagerDutyRuleset(), "pagerduty_team_members": dataSourcePagerDutyTeamMembers(), }, diff --git a/pagerdutyplugin/data_source_pagerduty_user.go b/pagerdutyplugin/data_source_pagerduty_user.go new file mode 100644 index 000000000..3da786c6d --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_user.go @@ -0,0 +1,109 @@ +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 dataSourceUser struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceUser)(nil) + +func (*dataSourceUser) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_user" +} + +func (*dataSourceUser) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "email": schema.StringAttribute{Required: true}, + "description": schema.StringAttribute{Computed: true}, + "id": schema.StringAttribute{Computed: true}, + "job_title": schema.StringAttribute{Computed: true}, + "name": schema.StringAttribute{Computed: true}, + "role": schema.StringAttribute{Computed: true}, + "time_zone": schema.StringAttribute{Computed: true}, + }, + } +} + +func (d *dataSourceUser) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceUser) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty user") + + var searchEmail types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("email"), &searchEmail)...) + if resp.Diagnostics.HasError() { + return + } + opts := pagerduty.ListUsersOptions{Query: searchEmail.ValueString()} + + var found *pagerduty.User + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := d.client.ListUsersWithContext(ctx, opts) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + + for _, user := range response.Users { + // if strings.EqualFold(user.Email, searchEmail.ValueString()) { + if user.Email == searchEmail.ValueString() { + found = &user + break + } + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user %s", searchEmail), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any user with the email: %s", searchEmail), + "", + ) + return + } + + model := dataSourceUserModel{ + Email: types.StringValue(found.Email), + Description: types.StringValue(found.Description), + ID: types.StringValue(found.ID), + JobTitle: types.StringValue(found.JobTitle), + Name: types.StringValue(found.Name), + Role: types.StringValue(found.Role), + Timezone: types.StringValue(found.Timezone), + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceUserModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + Description types.String `tfsdk:"description"` + JobTitle types.String `tfsdk:"job_title"` + Role types.String `tfsdk:"role"` + Timezone types.String `tfsdk:"time_zone"` +} diff --git a/pagerduty/data_source_pagerduty_user_test.go b/pagerdutyplugin/data_source_pagerduty_user_test.go similarity index 94% rename from pagerduty/data_source_pagerduty_user_test.go rename to pagerdutyplugin/data_source_pagerduty_user_test.go index 06244592c..16ff5189f 100644 --- a/pagerduty/data_source_pagerduty_user_test.go +++ b/pagerdutyplugin/data_source_pagerduty_user_test.go @@ -18,8 +18,8 @@ func TestAccDataSourcePagerDutyUser_Basic(t *testing.T) { description := fmt.Sprintf("%s-description", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), Steps: []resource.TestStep{ { Config: testAccDataSourcePagerDutyUserConfig(username, email, jobTitle, timeZone, role, description), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 738796ba1..fa6ec578a 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -64,6 +64,7 @@ func (p *Provider) DataSources(_ context.Context) [](func() datasource.DataSourc func() datasource.DataSource { return &dataSourceStandards{} }, func() datasource.DataSource { return &dataSourceTag{} }, func() datasource.DataSource { return &dataSourceUsers{} }, + func() datasource.DataSource { return &dataSourceUser{} }, func() datasource.DataSource { return &dataSourceVendor{} }, } } From 8ee82935abee6988997fefc7c2a0af28390248de Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Thu, 21 Mar 2024 04:44:47 -0300 Subject: [PATCH 4/8] Migrate resource user contact method --- ...port_pagerduty_user_contact_method_test.go | 6 +- pagerdutyplugin/provider_test.go | 22 +- .../resource_pagerduty_user_contact_method.go | 283 ++++++++++++++++++ ...urce_pagerduty_user_contact_method_test.go | 56 ++-- util/string_descriptor.go | 13 + util/validate_contact_address.go | 88 ++++++ .../resource/schema/booldefault/doc.go | 5 + .../schema/booldefault/static_value.go | 42 +++ vendor/modules.txt | 1 + 9 files changed, 481 insertions(+), 35 deletions(-) rename {pagerduty => pagerdutyplugin}/import_pagerduty_user_contact_method_test.go (84%) create mode 100644 pagerdutyplugin/resource_pagerduty_user_contact_method.go rename {pagerduty => pagerdutyplugin}/resource_pagerduty_user_contact_method_test.go (86%) create mode 100644 util/string_descriptor.go create mode 100644 util/validate_contact_address.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/doc.go create mode 100644 vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/static_value.go diff --git a/pagerduty/import_pagerduty_user_contact_method_test.go b/pagerdutyplugin/import_pagerduty_user_contact_method_test.go similarity index 84% rename from pagerduty/import_pagerduty_user_contact_method_test.go rename to pagerdutyplugin/import_pagerduty_user_contact_method_test.go index afd754c55..4abb63247 100644 --- a/pagerduty/import_pagerduty_user_contact_method_test.go +++ b/pagerdutyplugin/import_pagerduty_user_contact_method_test.go @@ -14,9 +14,9 @@ func TestAccPagerDutyUserContactMethod_import(t *testing.T) { email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodEmailConfig(username, email), diff --git a/pagerdutyplugin/provider_test.go b/pagerdutyplugin/provider_test.go index de285753d..4735d96f0 100644 --- a/pagerdutyplugin/provider_test.go +++ b/pagerdutyplugin/provider_test.go @@ -2,10 +2,8 @@ package pagerduty import ( "context" - "os" - "testing" - "time" - + "fmt" + "github.com/PagerDuty/go-pagerduty" pd "github.com/PagerDuty/terraform-provider-pagerduty/pagerduty" "github.com/PagerDuty/terraform-provider-pagerduty/util" "github.com/hashicorp/terraform-plugin-framework/providerserver" @@ -13,6 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "os" + "testing" + "time" ) func TestMain(m *testing.M) { @@ -102,3 +103,16 @@ func testAccPreCheckPagerDutyAbility(t *testing.T, ability string) { t.Skipf("Missing ability: %s. Skipping test", ability) } } + +func testAccCheckPagerDutyUserDestroy(s *terraform.State) error { + for _, r := range s.RootModule().Resources { + if r.Type != "pagerduty_user" { + continue + } + ctx := context.Background() + if _, err := testAccProvider.client.GetUserWithContext(ctx, r.Primary.ID, pagerduty.GetUserOptions{}); err == nil { + return fmt.Errorf("User still exists") + } + } + return nil +} diff --git a/pagerdutyplugin/resource_pagerduty_user_contact_method.go b/pagerdutyplugin/resource_pagerduty_user_contact_method.go new file mode 100644 index 000000000..6eaa0a78d --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_user_contact_method.go @@ -0,0 +1,283 @@ +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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceUserContactMethod struct{ client *pagerduty.Client } + +var ( + _ resource.ResourceWithConfigure = (*resourceUserContactMethod)(nil) + _ resource.ResourceWithImportState = (*resourceUserContactMethod)(nil) +) + +func (r *resourceUserContactMethod) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_user_contact_method" +} + +func (r *resourceUserContactMethod) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "user_id": schema.StringAttribute{Required: true}, + "label": schema.StringAttribute{Required: true}, + "country_code": schema.Int64Attribute{Optional: true, Computed: true}, + "enabled": schema.BoolAttribute{Computed: true}, + "blacklisted": schema.BoolAttribute{Computed: true}, + "address": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + util.ValidateContactAddress("type", "country_code"), + }, + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + "email_contact_method", + "phone_contact_method", + "push_notification_contact_method", + "sms_contact_method", + ), + }, + }, + "send_short_email": schema.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(false), + }, + }, + } +} + +func (r *resourceUserContactMethod) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceUserContactMethodModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + plan := buildPagerdutyContactMethod(&model) + log.Printf("[INFO] Creating PagerDuty user contact method %s", plan.Label) + + response, err := r.client.CreateUserContactMethodWithContext(ctx, plan.UserID, plan.ContactMethod) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error creating PagerDuty user contact method %s", plan.Label), + err.Error(), + ) + return + } + + model, err = requestGetUserContactMethod(ctx, r.client, plan.UserID, response.ID, true, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user contact method %s", plan.ID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceUserContactMethod) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id types.String + var userID types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("user_id"), &userID)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Reading PagerDuty user contact method %s", id) + + state, err := requestGetUserContactMethod(ctx, r.client, userID.ValueString(), id.ValueString(), false, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user contact method %s", id), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceUserContactMethod) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model resourceUserContactMethodModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + plan := buildPagerdutyContactMethod(&model) + if plan.ID == "" { + var id string + req.State.GetAttribute(ctx, path.Root("id"), &id) + plan.ID = id + } + log.Printf("[INFO] Updating PagerDuty user contact method %s", plan.ID) + + _, err := r.client.UpdateUserContactMethodWthContext(ctx, plan.UserID, plan.ContactMethod) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty user contact method %s", plan.ID), + err.Error(), + ) + return + } + + model, err = requestGetUserContactMethod(ctx, r.client, plan.UserID, plan.ID, true, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user contact method %s", plan.ID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceUserContactMethod) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var id types.String + var userID types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("user_id"), &userID)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Deleting PagerDuty user contact method %s", id) + + err := r.client.DeleteUserContactMethodWithContext(ctx, userID.ValueString(), id.ValueString()) + if err != nil && !util.IsNotFoundError(err) { + resp.Diagnostics.AddError( + fmt.Sprintf("Error deleting PagerDuty user contact method %s", id), + err.Error(), + ) + return + } + resp.State.RemoveResource(ctx) +} + +func (r *resourceUserContactMethod) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceUserContactMethod) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + ids := strings.Split(req.ID, ":") + if len(ids) != 2 { + resp.Diagnostics.AddError( + "Error importing PagerDuty user contact method", + "Expecting an ID formed as ':'", + ) + } + uid, id := ids[0], ids[1] + + var d diag.Diagnostics + d = resp.State.SetAttribute(ctx, path.Root("id"), types.StringValue(id)) + resp.Diagnostics.Append(d...) + d = resp.State.SetAttribute(ctx, path.Root("user_id"), types.StringValue(uid)) + resp.Diagnostics.Append(d...) + + // model, err := requestGetUserContactMethod(ctx, r.client, uid, id, true, &resp.Diagnostics) + // if err != nil { + // resp.Diagnostics.AddError( + // fmt.Sprintf("Error importing PagerDuty user contact method %s", req.ID), + // err.Error(), + // ) + // } + // + // resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type resourceUserContactMethodModel struct { + ID types.String + UserID types.String + Address types.String + Blacklisted types.Bool + CountryCode types.Int64 + Enabled types.Bool + Label types.String + SendShortEmail types.Bool + Type types.String +} + +type ContactMethod struct { + pagerduty.ContactMethod + UserID string +} + +func requestGetUserContactMethod(ctx context.Context, client *pagerduty.Client, userID, id string, retryNotFound bool, diags *diag.Diagnostics) (resourceUserContactMethodModel, error) { + var model resourceUserContactMethodModel + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + contactMethod, err := client.GetUserContactMethodWithContext(ctx, userID, id) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if !retryNotFound && util.IsNotFoundError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + model = flattenUserContactMethod(contactMethod, userID) + return nil + }) + + return model, err +} + +func buildPagerdutyContactMethod(model *resourceUserContactMethodModel) ContactMethod { + contactMethod := pagerduty.ContactMethod{ + Label: model.Label.ValueString(), + Address: model.Address.ValueString(), + SendShortEmail: model.SendShortEmail.ValueBool(), + Enabled: model.Enabled.ValueBool(), + } + contactMethod.Type = model.Type.ValueString() + + if !model.CountryCode.IsNull() && !model.CountryCode.IsUnknown() { + contactMethod.CountryCode = int(model.CountryCode.ValueInt64()) + } + + return ContactMethod{ContactMethod: contactMethod, UserID: model.UserID.ValueString()} +} + +func flattenUserContactMethod(response *pagerduty.ContactMethod, userID string) resourceUserContactMethodModel { + model := resourceUserContactMethodModel{ + ID: types.StringValue(response.ID), + Address: types.StringValue(response.Address), + Blacklisted: types.BoolValue(response.Blacklisted), + CountryCode: types.Int64Value(int64(response.CountryCode)), + Enabled: types.BoolValue(response.Enabled), + Label: types.StringValue(response.Label), + SendShortEmail: types.BoolValue(response.SendShortEmail), + Type: types.StringValue(response.Type), + UserID: types.StringValue(userID), + } + return model +} diff --git a/pagerduty/resource_pagerduty_user_contact_method_test.go b/pagerdutyplugin/resource_pagerduty_user_contact_method_test.go similarity index 86% rename from pagerduty/resource_pagerduty_user_contact_method_test.go rename to pagerdutyplugin/resource_pagerduty_user_contact_method_test.go index 5536479e1..55509a572 100644 --- a/pagerduty/resource_pagerduty_user_contact_method_test.go +++ b/pagerdutyplugin/resource_pagerduty_user_contact_method_test.go @@ -1,6 +1,7 @@ package pagerduty import ( + "context" "fmt" "regexp" "testing" @@ -17,9 +18,9 @@ func TestAccPagerDutyUserContactMethodEmail_Basic(t *testing.T) { emailUpdated := fmt.Sprintf("%s@foo.test", usernameUpdated) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodEmailConfig(username, email), @@ -44,9 +45,9 @@ func TestAccPagerDutyUserContactMethodPhone_Basic(t *testing.T) { emailUpdated := fmt.Sprintf("%s@foo.test", usernameUpdated) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodPhoneConfig(username, email, "4153013250"), @@ -56,7 +57,7 @@ func TestAccPagerDutyUserContactMethodPhone_Basic(t *testing.T) { }, { Config: testAccCheckPagerDutyUserContactMethodPhoneConfig(username, email, "04153013250"), - ExpectError: regexp.MustCompile("phone numbers starting with a 0 are not supported"), + ExpectError: regexp.MustCompile("Phone number can't start with a zero"), }, { Config: testAccCheckPagerDutyUserContactMethodPhoneConfig(usernameUpdated, emailUpdated, "8019351337"), @@ -74,9 +75,9 @@ func TestAccPagerDutyUserContactMethodPhone_FormatValidation(t *testing.T) { tooLongNumber := "4153013250415301325041530132504153013250,415301325041530132504,530132504153013250" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodPhoneFormatValidationConfig(username, email, "phone_contact_method", "1", tooLongNumber), @@ -109,9 +110,9 @@ func TestAccPagerDutyUserContactMethodPhone_EnforceUpdateIfAlreadyExist(t *testi newPhoneNumber := "4153013251" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodPhoneConfig(username, email, phoneNumber), @@ -140,9 +141,9 @@ func TestAccPagerDutyUserContactMethodSMS_Basic(t *testing.T) { emailUpdated := fmt.Sprintf("%s@foo.test", usernameUpdated) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodSMSConfig(username, email), @@ -165,9 +166,9 @@ func TestAccPagerDutyUserContactMethodPhone_NoPermaDiffWhenOmittingCountryCode(t email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserContactMethodDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserContactMethodPhoneNoPermaDiffWhenOmittingCountryCodeConfig(username, email, "4153013250"), @@ -184,13 +185,14 @@ func TestAccPagerDutyUserContactMethodPhone_NoPermaDiffWhenOmittingCountryCode(t } func testAccCheckPagerDutyUserContactMethodDestroy(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() + ctx := context.Background() for _, r := range s.RootModule().Resources { if r.Type != "pagerduty_user_contact_method" { continue } - if _, _, err := client.Users.GetContactMethod(r.Primary.Attributes["user_id"], r.Primary.ID); err == nil { + _, err := testAccProvider.client.GetUserContactMethodWithContext(ctx, r.Primary.Attributes["user_id"], r.Primary.ID) + if err == nil { return fmt.Errorf("User contact method still exists") } @@ -209,9 +211,8 @@ func testAccCheckPagerDutyUserContactMethodExists(n string) resource.TestCheckFu return fmt.Errorf("No user contact method ID is set") } - client, _ := testAccProvider.Meta().(*Config).Client() - - found, _, err := client.Users.GetContactMethod(rs.Primary.Attributes["user_id"], rs.Primary.ID) + ctx := context.Background() + found, err := testAccProvider.client.GetUserContactMethodWithContext(ctx, rs.Primary.Attributes["user_id"], rs.Primary.ID) if err != nil { return err } @@ -237,15 +238,14 @@ func testAccAddPhoneContactOutsideTerraform(n, p string) resource.TestCheckFunc } userID := rs.Primary.Attributes["user_id"] - client, _ := testAccProvider.Meta().(*Config).Client() - - found, _, err := client.Users.GetContactMethod(userID, rs.Primary.ID) + ctx := context.Background() + found, err := testAccProvider.client.GetUserContactMethodWithContext(ctx, rs.Primary.Attributes["user_id"], rs.Primary.ID) if err != nil { return err } found.Address = p - _, _, err = client.Users.CreateContactMethod(userID, found) + _, err = testAccProvider.client.CreateUserContactMethodWithContext(ctx, userID, *found) if err != nil { return fmt.Errorf("was not possible to set phone %s contact number outside Terraform state: %v", p, err) } diff --git a/util/string_descriptor.go b/util/string_descriptor.go new file mode 100644 index 000000000..3f575ff1c --- /dev/null +++ b/util/string_descriptor.go @@ -0,0 +1,13 @@ +package util + +import "context" + +type stringDescriptor struct{ value string } + +func (d stringDescriptor) Description(ctx context.Context) string { + return d.MarkdownDescription(ctx) +} + +func (d stringDescriptor) MarkdownDescription(_ context.Context) string { + return d.value +} diff --git a/util/validate_contact_address.go b/util/validate_contact_address.go new file mode 100644 index 000000000..7886276fc --- /dev/null +++ b/util/validate_contact_address.go @@ -0,0 +1,88 @@ +package util + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ValidateContactAddress(typeKey, countryCodeKey string) validator.String { + return &contactAddressValidator{stringDescriptor{"TODO"}, typeKey, countryCodeKey} +} + +type contactAddressValidator struct { + stringDescriptor + typeKey, countryCodeKey string +} + +func (v contactAddressValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + var typeConfig types.String + d := req.Config.GetAttribute(ctx, path.Root(v.typeKey), &typeConfig) + resp.Diagnostics.Append(d...) + + var countryCode types.Int64 + d = req.Config.GetAttribute(ctx, path.Root(v.countryCodeKey), &countryCode) + resp.Diagnostics.Append(d...) + + if resp.Diagnostics.HasError() { + return + } + t := typeConfig.ValueString() + code := int(countryCode.ValueInt64()) + addr := req.ConfigValue.ValueString() + + if t == "sms_contact_method" || t == "phone_contact_method" { + // Validation logic based on https://support.pagerduty.com/docs/user-profile#phone-number-formatting + maxLength := 40 + + if len(addr) > maxLength { + resp.Diagnostics.AddError("phone numbers may not exceed 40 characters", addr) + return + } + + if !phoneOnlyAllowedChars.MatchString(addr) { + resp.Diagnostics.AddError( + "phone numbers may only include digits from 0-9 and the symbols: comma (,), asterisk (*), and pound (#)", + addr, + ) + return + } + + isMexicoNumber := code == 52 + if t == "sms_contact_method" && isMexicoNumber && strings.HasPrefix(addr, "1") { + resp.Diagnostics.AddError( + "Mexico-based SMS numbers should be free of area code prefixes", + fmt.Sprintf("Please remove the leading 1 in the number %q", addr), + ) + return + } + + isTrunkPrefixNotSupported := map[int]string{ + 33: "0", // France (33-0) + 40: "0", // Romania (40-0) + 44: "0", // UK (44-0) + 45: "0", // Denmark (45-0) + 49: "0", // Germany (49-0) + 61: "0", // Australia (61-0) + 66: "0", // Thailand (66-0) + 91: "0", // India (91-0) + 1: "1", // North America (1-1) + } + + prefix, ok := isTrunkPrefixNotSupported[code] + if ok && strings.HasPrefix(addr, prefix) { + resp.Diagnostics.AddError( + fmt.Sprintf("Trunk prefixes are not supported for following countries and regions: France, Romania, UK, Denmark, Germany, Australia, Thailand, India and North America, so must be formatted for international use without the leading %s", prefix), + "", + ) + return + } + } +} + +var phoneOnlyAllowedChars = regexp.MustCompile(`^[0-9,*#]+$`) diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/doc.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/doc.go new file mode 100644 index 000000000..fe6b0f76d --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package booldefault provides default values for types.Bool attributes. +package booldefault diff --git a/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/static_value.go b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/static_value.go new file mode 100644 index 000000000..797f81d09 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault/static_value.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package booldefault + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// StaticBool returns a static boolean value default handler. +// +// Use StaticBool if a static default value for a boolean should be set. +func StaticBool(defaultVal bool) defaults.Bool { + return staticBoolDefault{ + defaultVal: defaultVal, + } +} + +// staticBoolDefault is static value default handler that +// sets a value on a boolean attribute. +type staticBoolDefault struct { + defaultVal bool +} + +// Description returns a human-readable description of the default value handler. +func (d staticBoolDefault) Description(_ context.Context) string { + return fmt.Sprintf("value defaults to %t", d.defaultVal) +} + +// MarkdownDescription returns a markdown description of the default value handler. +func (d staticBoolDefault) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value defaults to `%t`", d.defaultVal) +} + +// DefaultBool implements the static default value logic. +func (d staticBoolDefault) DefaultBool(_ context.Context, req defaults.BoolRequest, resp *defaults.BoolResponse) { + resp.PlanValue = types.BoolValue(d.defaultVal) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 26f324e00..33a37fc8b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -164,6 +164,7 @@ github.com/hashicorp/terraform-plugin-framework/provider/schema github.com/hashicorp/terraform-plugin-framework/providerserver github.com/hashicorp/terraform-plugin-framework/resource github.com/hashicorp/terraform-plugin-framework/resource/schema +github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier From 417ecd316e18ccd7398f21ef5888454d7ab6f7e4 Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Fri, 22 Mar 2024 11:05:42 -0300 Subject: [PATCH 5/8] Migrate resource user notification rule --- pagerduty/provider.go | 1 - ...source_pagerduty_user_notification_rule.go | 235 ----------- ...t_pagerduty_user_notification_rule_test.go | 6 +- pagerdutyplugin/provider.go | 1 + ...source_pagerduty_user_notification_rule.go | 367 ++++++++++++++++++ ...e_pagerduty_user_notification_rule_test.go | 71 ++-- 6 files changed, 409 insertions(+), 272 deletions(-) delete mode 100644 pagerduty/resource_pagerduty_user_notification_rule.go rename {pagerduty => pagerdutyplugin}/import_pagerduty_user_notification_rule_test.go (85%) create mode 100644 pagerdutyplugin/resource_pagerduty_user_notification_rule.go rename {pagerduty => pagerdutyplugin}/resource_pagerduty_user_notification_rule_test.go (78%) diff --git a/pagerduty/provider.go b/pagerduty/provider.go index ee7b8cdd5..e9c68fd95 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -120,7 +120,6 @@ func Provider(isMux bool) *schema.Provider { "pagerduty_team_membership": resourcePagerDutyTeamMembership(), "pagerduty_user": resourcePagerDutyUser(), "pagerduty_user_contact_method": resourcePagerDutyUserContactMethod(), - "pagerduty_user_notification_rule": resourcePagerDutyUserNotificationRule(), "pagerduty_event_rule": resourcePagerDutyEventRule(), "pagerduty_ruleset": resourcePagerDutyRuleset(), "pagerduty_ruleset_rule": resourcePagerDutyRulesetRule(), diff --git a/pagerduty/resource_pagerduty_user_notification_rule.go b/pagerduty/resource_pagerduty_user_notification_rule.go deleted file mode 100644 index c180e0ab7..000000000 --- a/pagerduty/resource_pagerduty_user_notification_rule.go +++ /dev/null @@ -1,235 +0,0 @@ -package pagerduty - -import ( - "fmt" - "log" - "net/http" - "regexp" - "strings" - "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" -) - -func resourcePagerDutyUserNotificationRule() *schema.Resource { - return &schema.Resource{ - Create: resourcePagerDutyUserNotificationRuleCreate, - Read: resourcePagerDutyUserNotificationRuleRead, - Update: resourcePagerDutyUserNotificationRuleUpdate, - Delete: resourcePagerDutyUserNotificationRuleDelete, - Importer: &schema.ResourceImporter{ - State: resourcePagerDutyUserNotificationRuleImport, - }, - Schema: map[string]*schema.Schema{ - "user_id": { - Type: schema.TypeString, - Required: true, - }, - - "start_delay_in_minutes": { - Type: schema.TypeInt, - Required: true, - }, - - "urgency": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validateValueDiagFunc([]string{ - "high", - "low", - }), - }, - "contact_method": { - Required: true, - Type: schema.TypeMap, - // Using the `Elem` block to define specific keys for the map is currently not possible. - // The workaround described in SDK documentation is to confirm the required keys are set when expanding the Map object inside the resource code. - // See https://www.terraform.io/docs/extend/schemas/schema-types.html#typemap - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - ValidateDiagFunc: validation.MapKeyMatch(regexp.MustCompile("(id|type)"), "`contact_method` must only have `id` and `types` attributes"), - }, - }, - } -} - -func buildUserNotificationRuleStruct(d *schema.ResourceData) (*pagerduty.NotificationRule, error) { - contactMethod, err := expandContactMethod(d.Get("contact_method")) - if err != nil { - return nil, err - } - notificationRule := &pagerduty.NotificationRule{ - Type: "assignment_notification_rule", - StartDelayInMinutes: d.Get("start_delay_in_minutes").(int), - Urgency: d.Get("urgency").(string), - ContactMethod: contactMethod, - } - - return notificationRule, nil -} - -func fetchPagerDutyUserNotificationRule(d *schema.ResourceData, meta interface{}, errCallback func(error, *schema.ResourceData) error) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - userID := d.Get("user_id").(string) - - return retry.Retry(2*time.Minute, func() *retry.RetryError { - resp, _, err := client.Users.GetNotificationRule(userID, 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("urgency", resp.Urgency) - d.Set("start_delay_in_minutes", resp.StartDelayInMinutes) - d.Set("contact_method", flattenContactMethod(resp.ContactMethod)) - - return nil - }) -} - -func resourcePagerDutyUserNotificationRuleCreate(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - userID := d.Get("user_id").(string) - - notificationRule, err := buildUserNotificationRuleStruct(d) - if err != nil { - return err - } - - resp, _, err := client.Users.CreateNotificationRule(userID, notificationRule) - if err != nil { - return err - } - - d.SetId(resp.ID) - - return fetchPagerDutyUserNotificationRule(d, meta, genError) -} - -func resourcePagerDutyUserNotificationRuleRead(d *schema.ResourceData, meta interface{}) error { - return fetchPagerDutyUserNotificationRule(d, meta, handleNotFoundError) -} - -func resourcePagerDutyUserNotificationRuleUpdate(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - notificationRule, err := buildUserNotificationRuleStruct(d) - if err != nil { - return err - } - - log.Printf("[INFO] Updating PagerDuty user notification rule %s", d.Id()) - - userID := d.Get("user_id").(string) - - if _, _, err := client.Users.UpdateNotificationRule(userID, d.Id(), notificationRule); err != nil { - return err - } - - return resourcePagerDutyUserNotificationRuleRead(d, meta) -} - -func resourcePagerDutyUserNotificationRuleDelete(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - log.Printf("[INFO] Deleting PagerDuty user notification rule %s", d.Id()) - - userID := d.Get("user_id").(string) - - if _, err := client.Users.DeleteNotificationRule(userID, d.Id()); err != nil { - return handleNotFoundError(err, d) - } - - d.SetId("") - - return nil -} - -func resourcePagerDutyUserNotificationRuleImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - client, err := meta.(*Config).Client() - if err != nil { - return []*schema.ResourceData{}, err - } - - ids := strings.Split(d.Id(), ":") - - if len(ids) != 2 { - return []*schema.ResourceData{}, fmt.Errorf("Error importing pagerduty_user_notification_rule. Expecting an ID formed as '.'") - } - uid, id := ids[0], ids[1] - - _, _, err = client.Users.GetNotificationRule(uid, id) - if err != nil { - return []*schema.ResourceData{}, err - } - - d.SetId(id) - d.Set("user_id", uid) - - return []*schema.ResourceData{d}, nil -} - -func expandContactMethod(v interface{}) (*pagerduty.ContactMethodReference, error) { - cm := v.(map[string]interface{}) - - if _, ok := cm["id"]; !ok { - return nil, fmt.Errorf("the `id` attribute of `contact_method` is required") - } - - if t, ok := cm["type"]; !ok { - return nil, fmt.Errorf("the `type` attribute of `contact_method` is required") - } else { - switch t { - case "email_contact_method": - case "phone_contact_method": - case "push_notification_contact_method": - case "sms_contact_method": - // Valid - default: - return nil, fmt.Errorf("the `type` attribute of `contact_method` must be one of `email_contact_method`, `phone_contact_method`, `push_notification_contact_method` or `sms_contact_method`") - } - } - - contactMethod := &pagerduty.ContactMethodReference{ - ID: cm["id"].(string), - Type: cm["type"].(string), - } - - return contactMethod, nil -} - -func flattenContactMethod(v *pagerduty.ContactMethodReference) map[string]interface{} { - contactMethod := map[string]interface{}{ - "id": v.ID, - "type": v.Type, - } - - return contactMethod -} diff --git a/pagerduty/import_pagerduty_user_notification_rule_test.go b/pagerdutyplugin/import_pagerduty_user_notification_rule_test.go similarity index 85% rename from pagerduty/import_pagerduty_user_notification_rule_test.go rename to pagerdutyplugin/import_pagerduty_user_notification_rule_test.go index 134d95648..e7770444b 100644 --- a/pagerduty/import_pagerduty_user_notification_rule_test.go +++ b/pagerdutyplugin/import_pagerduty_user_notification_rule_test.go @@ -15,9 +15,9 @@ func TestAccPagerDutyUserNotificationRule_import(t *testing.T) { contactMethodType := "phone_contact_method" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig(contactMethodType, username, email), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index fa6ec578a..01048c606 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -80,6 +80,7 @@ func (p *Provider) Resources(_ context.Context) [](func() resource.Resource) { func() resource.Resource { return &resourceTag{} }, func() resource.Resource { return &resourceTeam{} }, func() resource.Resource { return &resourceUserHandoffNotificationRule{} }, + func() resource.Resource { return &resourceUserNotificationRule{} }, } } diff --git a/pagerdutyplugin/resource_pagerduty_user_notification_rule.go b/pagerdutyplugin/resource_pagerduty_user_notification_rule.go new file mode 100644 index 000000000..397310367 --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_user_notification_rule.go @@ -0,0 +1,367 @@ +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/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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceUserNotificationRule struct{ client *pagerduty.Client } + +var ( + _ resource.ResourceWithConfigure = (*resourceUserNotificationRule)(nil) + _ resource.ResourceWithImportState = (*resourceUserNotificationRule)(nil) +) + +func (r *resourceUserNotificationRule) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_user_notification_rule" +} + +func (r *resourceUserNotificationRule) 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()}, + }, + "user_id": schema.StringAttribute{Required: true}, + "start_delay_in_minutes": schema.Int64Attribute{Required: true}, + "urgency": schema.StringAttribute{ + Required: true, + Validators: []validator.String{stringvalidator.OneOf("high", "low")}, + }, + }, + Blocks: map[string]schema.Block{ + "contact_method": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + "email_contact_method", + "phone_contact_method", + "push_notification_contact_method", + "sms_contact_method", + ), + }, + }, + "id": schema.StringAttribute{Required: true}, + }, + }, + }, + } +} + +func (r *resourceUserNotificationRule) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model resourceUserNotificationRuleModel + var userID types.String + + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("user_id"), &userID)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + plan := buildPagerdutyUserNotificationRule(ctx, &model, &resp.Diagnostics) + log.Printf("[INFO] Creating PagerDuty user notification rule for %s", userID) + + response, err := r.client.CreateUserNotificationRuleWithContext(ctx, userID.ValueString(), plan) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error creating PagerDuty user notification rule for %s", userID), + err.Error(), + ) + return + } + + model, err = requestGetUserNotificationRule(ctx, r.client, userID.ValueString(), response.ID, true, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user notification rule for %s", userID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceUserNotificationRule) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id types.String + var userID types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("user_id"), &userID)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + + log.Printf("[INFO] Reading PagerDuty user notification rule %s", id) + + state, err := requestGetUserNotificationRule(ctx, r.client, userID.ValueString(), id.ValueString(), false, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user notification rule %s", id), + err.Error(), + ) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceUserNotificationRule) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model resourceUserNotificationRuleModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + plan := buildPagerdutyUserNotificationRule(ctx, &model, &resp.Diagnostics) + if plan.ID == "" { + var id string + req.State.GetAttribute(ctx, path.Root("id"), &id) + plan.ID = id + } + + var userIDState types.String + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("user_id"), &userIDState)...) + if resp.Diagnostics.HasError() { + return + } + userID := userIDState.ValueString() + + if userID == "" { + var userIDPlan types.String + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("user_id"), &userIDPlan)...) + if resp.Diagnostics.HasError() { + return + } + userID = userIDPlan.ValueString() + } + log.Printf("[INFO] Updating PagerDuty user notification rule %s", plan.ID) + + _, err := r.client.UpdateUserNotificationRuleWithContext(ctx, userID, plan) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty user notification rule %s", plan.ID), + err.Error(), + ) + return + } + + model, err = requestGetUserNotificationRule(ctx, r.client, userID, plan.ID, false, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty user notification rule %s", plan.ID), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +func (r *resourceUserNotificationRule) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var id types.String + var userID types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("user_id"), &userID)...) + if resp.Diagnostics.HasError() { + return + } + + log.Printf("[INFO] Deleting PagerDuty user notification rule %s", id) + + err := r.client.DeleteUserNotificationRuleWithContext(ctx, userID.ValueString(), id.ValueString()) + if err != nil && !util.IsNotFoundError(err) { + resp.Diagnostics.AddError( + fmt.Sprintf("Error deleting PagerDuty user notification rule %s", id), + err.Error(), + ) + return + } + resp.State.RemoveResource(ctx) +} + +func (r *resourceUserNotificationRule) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceUserNotificationRule) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + ids := strings.Split(req.ID, ":") + if len(ids) != 2 { + resp.Diagnostics.AddError( + "Error importing pagerduty_user_notification_rule", + "Expecting an ID formed as '.'", + ) + return + } + uid, id := ids[0], ids[1] + + model, err := requestGetUserNotificationRule(ctx, r.client, uid, id, true, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error importing PagerDuty user notification rule %s", id), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type resourceUserNotificationRuleModel struct { + ID types.String `tfsdk:"id"` + UserID types.String `tfsdk:"user_id"` + StartDelayInMinutes types.Int64 `tfsdk:"start_delay_in_minutes"` + Urgency types.String `tfsdk:"urgency"` + ContactMethod types.Object `tfsdk:"contact_method"` +} + +func requestGetUserNotificationRule(ctx context.Context, client *pagerduty.Client, userID string, id string, retryNotFound bool, diags *diag.Diagnostics) (resourceUserNotificationRuleModel, error) { + var model resourceUserNotificationRuleModel + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + notificationRule, err := client.GetUserNotificationRuleWithContext(ctx, userID, id) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if !retryNotFound && util.IsNotFoundError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + model = flattenUserNotificationRule(notificationRule, userID) + return nil + }) + + return model, err +} + +func buildPagerdutyUserNotificationRule(ctx context.Context, model *resourceUserNotificationRuleModel, diags *diag.Diagnostics) pagerduty.NotificationRule { + return pagerduty.NotificationRule{ + ID: model.ID.ValueString(), + ContactMethod: buildPagerDutyContactMethodReference(ctx, model.ContactMethod, diags), + StartDelayInMinutes: uint(model.StartDelayInMinutes.ValueInt64()), + Type: "assignment_notification_rule", + Urgency: model.Urgency.ValueString(), + } +} + +func buildPagerDutyContactMethodReference(ctx context.Context, contactMethod types.Object, diags *diag.Diagnostics) pagerduty.ContactMethod { + var target struct { + ID types.String `tfsdk:"id"` + Type types.String `tfsdk:"type"` + } + + d := contactMethod.As(ctx, &target, basetypes.ObjectAsOptions{}) + diags.Append(d...) + + return pagerduty.ContactMethod{ + ID: target.ID.ValueString(), + Type: target.Type.ValueString(), + } +} + +func flattenUserNotificationRule(response *pagerduty.NotificationRule, userID string) resourceUserNotificationRuleModel { + contactMethodObjectType := types.ObjectType{AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + }} + model := resourceUserNotificationRuleModel{ + ID: types.StringValue(response.ID), + StartDelayInMinutes: types.Int64Value(int64(response.StartDelayInMinutes)), + Urgency: types.StringValue(response.Urgency), + UserID: types.StringValue(userID), + ContactMethod: types.ObjectValueMust(contactMethodObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(response.ContactMethod.ID), + "type": types.StringValue(response.ContactMethod.Type), + }), + } + return model +} + +/* + +func resourcePagerDutyUserNotificationRuleImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + client, err := meta.(*Config).Client() + if err != nil { + return []*schema.ResourceData{}, err + } + + ids := strings.Split(d.Id(), ":") + + if len(ids) != 2 { + return []*schema.ResourceData{}, fmt.Errorf("Error importing pagerduty_user_notification_rule. Expecting an ID formed as '.'") + } + uid, id := ids[0], ids[1] + + _, _, err = client.Users.GetNotificationRule(uid, id) + if err != nil { + return []*schema.ResourceData{}, err + } + + d.SetId(id) + d.Set("user_id", uid) + + return []*schema.ResourceData{d}, nil +} + +func expandContactMethod(v interface{}) (*pagerduty.ContactMethodReference, error) { + cm := v.(map[string]interface{}) + if _, ok := cm["id"]; !ok { + return nil, fmt.Errorf("the `id` attribute of `contact_method` is required") + } + if t, ok := cm["type"]; !ok { + return nil, fmt.Errorf("the `type` attribute of `contact_method` is required") + } else { + switch t { + case "email_contact_method": + case "phone_contact_method": + case "push_notification_contact_method": + case "sms_contact_method": + // Valid + default: + return nil, fmt.Errorf("the `type` attribute of `contact_method` must be one of `email_contact_method`, `phone_contact_method`, `push_notification_contact_method` or `sms_co> + } + } + contactMethod := &pagerduty.ContactMethodReference{ + ID: cm["id"].(string), + Type: cm["type"].(string), + } + return contactMethod, nil +} + +*/ diff --git a/pagerduty/resource_pagerduty_user_notification_rule_test.go b/pagerdutyplugin/resource_pagerduty_user_notification_rule_test.go similarity index 78% rename from pagerduty/resource_pagerduty_user_notification_rule_test.go rename to pagerdutyplugin/resource_pagerduty_user_notification_rule_test.go index adc6bb55d..c970af6da 100644 --- a/pagerduty/resource_pagerduty_user_notification_rule_test.go +++ b/pagerdutyplugin/resource_pagerduty_user_notification_rule_test.go @@ -1,6 +1,7 @@ package pagerduty import ( + "context" "fmt" "regexp" "testing" @@ -18,9 +19,9 @@ func TestAccPagerDutyUserNotificationRuleContactMethod_Basic(t *testing.T) { contactMethodType3 := "sms_contact_method" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig(contactMethodType1, username, email), @@ -49,13 +50,17 @@ func TestAccPagerDutyUserNotificationRuleContactMethod_Invalid(t *testing.T) { email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, Steps: []resource.TestStep{ { - Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig_Invalid(username, email), - ExpectError: regexp.MustCompile("the `type` attribute of `contact_method` must be one of `email_contact_method`, `phone_contact_method`, `push_notification_contact_method` or `sms_contact_method`"), + Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig_Invalid(username, email), + ExpectError: regexp.MustCompile( + `Attribute contact_method.type value must be one of: \["email_contact_method"` + + `\s+"phone_contact_method" "push_notification_contact_method"` + + `\s+"sms_contact_method"\], got: "invalid_contact_method`, + ), }, }, }) @@ -66,13 +71,13 @@ func TestAccPagerDutyUserNotificationRuleContactMethod_Missing_id(t *testing.T) email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig_Missing_id(username, email), - ExpectError: regexp.MustCompile("the `id` attribute of `contact_method` is required"), + ExpectError: regexp.MustCompile(`The argument "id" is required, but no definition was found.`), }, }, }) @@ -83,13 +88,13 @@ func TestAccPagerDutyUserNotificationRuleContactMethod_Missing_type(t *testing.T email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig_Missing_type(username, email), - ExpectError: regexp.MustCompile("the `type` attribute of `contact_method` is required"), + ExpectError: regexp.MustCompile(`The argument "type" is required, but no definition was found.`), }, }, }) @@ -100,30 +105,31 @@ func TestAccPagerDutyUserNotificationRuleContactMethod_Unknown_key(t *testing.T) email := fmt.Sprintf("%s@foo.test", username) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyUserNotificationRuleDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyUserNotificationRuleContactMethodConfig_Unknown_key(username, email), - ExpectError: regexp.MustCompile("`contact_method` must only have `id` and `types` attributes: foo"), + ExpectError: regexp.MustCompile(`An argument named "foo" is not expected here.`), }, }, }) } func testAccCheckPagerDutyUserNotificationRuleDestroy(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() for _, r := range s.RootModule().Resources { if r.Type != "pagerduty_user_notification_rule" { continue } - if _, _, err := client.Users.GetNotificationRule(r.Primary.Attributes["user_id"], r.Primary.ID); err == nil { + client := testAccProvider.client + ctx := context.Background() + if _, err := client.GetUserNotificationRuleWithContext(ctx, r.Primary.Attributes["user_id"], r.Primary.ID); err == nil { return fmt.Errorf("User notification rule still exists") } - } + return nil } @@ -138,9 +144,9 @@ func testAccCheckPagerDutyUserNotificationRuleExists(n string) resource.TestChec return fmt.Errorf("No user notification rule ID is set") } - client, _ := testAccProvider.Meta().(*Config).Client() - - found, _, err := client.Users.GetNotificationRule(rs.Primary.Attributes["user_id"], rs.Primary.ID) + client := testAccProvider.client + ctx := context.Background() + found, err := client.GetUserNotificationRuleWithContext(ctx, rs.Primary.Attributes["user_id"], rs.Primary.ID) if err != nil { return err } @@ -160,7 +166,7 @@ resource "pagerduty_user_notification_rule" "foo" { start_delay_in_minutes = 1 urgency = "high" - contact_method = { + contact_method { type = "%[1]v" id = pagerduty_user_contact_method.%[1]v.id } @@ -207,7 +213,7 @@ resource "pagerduty_user_notification_rule" "foo" { start_delay_in_minutes = 1 urgency = "high" - contact_method = { + contact_method { type = "invalid_contact_method" id = pagerduty_user_contact_method.email_contact_method.id } @@ -238,7 +244,7 @@ resource "pagerduty_user_notification_rule" "foo" { start_delay_in_minutes = 1 urgency = "high" - contact_method = { + contact_method { type = "invalid_contact_method" } } @@ -261,7 +267,7 @@ resource "pagerduty_user_notification_rule" "foo" { start_delay_in_minutes = 1 urgency = "high" - contact_method = { + contact_method { id = pagerduty_user_contact_method.email_contact_method.id } } @@ -291,11 +297,10 @@ resource "pagerduty_user_notification_rule" "foo" { start_delay_in_minutes = 1 urgency = "high" - contact_method = { + contact_method { type = pagerduty_user_contact_method.email_contact_method.type id = pagerduty_user_contact_method.email_contact_method.id - - foo = "bar" + foo = "bar" } } From 43134b1c9dc87488a5047dea20ffc991846d427d Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Thu, 28 Mar 2024 08:18:22 -0300 Subject: [PATCH 6/8] Migrate data source escalation policy --- ...data_source_pagerduty_escalation_policy.go | 1 + pagerduty/provider.go | 1 + ...data_source_pagerduty_escalation_policy.go | 89 +++++++++++++++++++ ...source_pagerduty_escalation_policy_test.go | 4 +- pagerdutyplugin/provider.go | 1 + 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 pagerdutyplugin/data_source_pagerduty_escalation_policy.go rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_escalation_policy_test.go (94%) diff --git a/pagerduty/data_source_pagerduty_escalation_policy.go b/pagerduty/data_source_pagerduty_escalation_policy.go index 503b55a65..00bd60752 100644 --- a/pagerduty/data_source_pagerduty_escalation_policy.go +++ b/pagerduty/data_source_pagerduty_escalation_policy.go @@ -11,6 +11,7 @@ import ( "github.com/heimweh/go-pagerduty/pagerduty" ) +// Deprecated: Migrated to pagerdutyplugin.dataSourceEscalationPolicy. Kept for testing purposes. func dataSourcePagerDutyEscalationPolicy() *schema.Resource { return &schema.Resource{ Read: dataSourcePagerDutyEscalationPolicyRead, diff --git a/pagerduty/provider.go b/pagerduty/provider.go index e9c68fd95..bbebc8d47 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -151,6 +151,7 @@ func Provider(isMux bool) *schema.Provider { if isMux { delete(p.DataSourcesMap, "pagerduty_business_service") + delete(p.DataSourcesMap, "pagerduty_escalation_policy") delete(p.DataSourcesMap, "pagerduty_licenses") delete(p.DataSourcesMap, "pagerduty_priority") delete(p.DataSourcesMap, "pagerduty_service") diff --git a/pagerdutyplugin/data_source_pagerduty_escalation_policy.go b/pagerdutyplugin/data_source_pagerduty_escalation_policy.go new file mode 100644 index 000000000..d498818ca --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_escalation_policy.go @@ -0,0 +1,89 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/terraform-provider-pagerduty/util/apiutil" + "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" +) + +type dataSourceEscalationPolicy struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceEscalationPolicy)(nil) + +func (*dataSourceEscalationPolicy) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_escalation_policy" +} + +func (*dataSourceEscalationPolicy) 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}, + }, + } +} + +func (d *dataSourceEscalationPolicy) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceEscalationPolicy) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty escalation policy") + + var searchName types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("name"), &searchName)...) + if resp.Diagnostics.HasError() { + return + } + opts := pagerduty.ListEscalationPoliciesOptions{Query: searchName.ValueString()} + + var found *pagerduty.EscalationPolicy + err := apiutil.All(ctx, func(offset int) (bool, error) { + resp, err := d.client.ListEscalationPoliciesWithContext(ctx, opts) + if err != nil { + return false, err + } + + for _, escalationPolicy := range resp.EscalationPolicies { + if escalationPolicy.Name == searchName.ValueString() { + found = &escalationPolicy + return false, nil + } + } + + return resp.More, nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error searching PagerDuty escalation policy %s", searchName), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any escalation policy with the name: %s", searchName), + "", + ) + return + } + + model := dataSourceEscalationPolicyModel{ + ID: types.StringValue(found.ID), + Name: types.StringValue(found.Name), + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceEscalationPolicyModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} diff --git a/pagerduty/data_source_pagerduty_escalation_policy_test.go b/pagerdutyplugin/data_source_pagerduty_escalation_policy_test.go similarity index 94% rename from pagerduty/data_source_pagerduty_escalation_policy_test.go rename to pagerdutyplugin/data_source_pagerduty_escalation_policy_test.go index 11dad1513..8ced25830 100644 --- a/pagerduty/data_source_pagerduty_escalation_policy_test.go +++ b/pagerdutyplugin/data_source_pagerduty_escalation_policy_test.go @@ -15,8 +15,8 @@ func TestAccDataSourcePagerDutyEscalationPolicy_Basic(t *testing.T) { escalationPolicy := 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: testAccDataSourcePagerDutyEscalationPolicyConfig(username, email, escalationPolicy), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 01048c606..6f480c5ef 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -53,6 +53,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro func (p *Provider) DataSources(_ context.Context) [](func() datasource.DataSource) { return [](func() datasource.DataSource){ func() datasource.DataSource { return &dataSourceBusinessService{} }, + func() datasource.DataSource { return &dataSourceEscalationPolicy{} }, func() datasource.DataSource { return &dataSourceExtensionSchema{} }, func() datasource.DataSource { return &dataSourceIntegration{} }, func() datasource.DataSource { return &dataSourceLicenses{} }, From 3b68ab11911b42463deef4fde7bbc70b050b0ec4 Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Thu, 28 Mar 2024 08:45:30 -0300 Subject: [PATCH 7/8] Migrate data source schedule --- pagerduty/data_source_pagerduty_schedule.go | 74 --------------- pagerduty/provider.go | 1 - .../data_source_pagerduty_schedule.go | 93 +++++++++++++++++++ .../data_source_pagerduty_schedule_test.go | 9 +- 4 files changed, 98 insertions(+), 79 deletions(-) delete mode 100644 pagerduty/data_source_pagerduty_schedule.go create mode 100644 pagerdutyplugin/data_source_pagerduty_schedule.go rename {pagerduty => pagerdutyplugin}/data_source_pagerduty_schedule_test.go (87%) diff --git a/pagerduty/data_source_pagerduty_schedule.go b/pagerduty/data_source_pagerduty_schedule.go deleted file mode 100644 index e60b63aca..000000000 --- a/pagerduty/data_source_pagerduty_schedule.go +++ /dev/null @@ -1,74 +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 dataSourcePagerDutySchedule() *schema.Resource { - return &schema.Resource{ - Read: dataSourcePagerDutyScheduleRead, - - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - }, - } -} - -func dataSourcePagerDutyScheduleRead(d *schema.ResourceData, meta interface{}) error { - client, err := meta.(*Config).Client() - if err != nil { - return err - } - - log.Printf("[INFO] Reading PagerDuty schedule") - - searchName := d.Get("name").(string) - - o := &pagerduty.ListSchedulesOptions{ - Query: searchName, - } - - return retry.Retry(5*time.Minute, func() *retry.RetryError { - resp, _, err := client.Schedules.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.Schedule - - for _, schedule := range resp.Schedules { - if schedule.Name == searchName { - found = schedule - break - } - } - - if found == nil { - return retry.NonRetryableError( - fmt.Errorf("Unable to locate any schedule with the name: %s", searchName), - ) - } - - d.SetId(found.ID) - d.Set("name", found.Name) - - return nil - }) -} diff --git a/pagerduty/provider.go b/pagerduty/provider.go index bbebc8d47..7281c95bd 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -87,7 +87,6 @@ func Provider(isMux bool) *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ "pagerduty_escalation_policy": dataSourcePagerDutyEscalationPolicy(), - "pagerduty_schedule": dataSourcePagerDutySchedule(), "pagerduty_licenses": dataSourcePagerDutyLicenses(), "pagerduty_user_contact_method": dataSourcePagerDutyUserContactMethod(), "pagerduty_team": dataSourcePagerDutyTeam(), diff --git a/pagerdutyplugin/data_source_pagerduty_schedule.go b/pagerdutyplugin/data_source_pagerduty_schedule.go new file mode 100644 index 000000000..68e931167 --- /dev/null +++ b/pagerdutyplugin/data_source_pagerduty_schedule.go @@ -0,0 +1,93 @@ +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 dataSourceSchedule struct{ client *pagerduty.Client } + +var _ datasource.DataSourceWithConfigure = (*dataSourceSchedule)(nil) + +func (*dataSourceSchedule) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "pagerduty_schedule" +} + +func (*dataSourceSchedule) 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}, + }, + } +} + +func (d *dataSourceSchedule) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&d.client, req.ProviderData)...) +} + +func (d *dataSourceSchedule) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + log.Println("[INFO] Reading PagerDuty schedule") + + var searchName types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("name"), &searchName)...) + if resp.Diagnostics.HasError() { + return + } + opts := pagerduty.ListSchedulesOptions{Query: searchName.ValueString()} + + var found *pagerduty.Schedule + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + response, err := d.client.ListSchedulesWithContext(ctx, opts) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + + for _, schedule := range response.Schedules { + if schedule.Name == searchName.ValueString() { + found = &schedule + break + } + } + return nil + }) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty schedule %s", searchName), + err.Error(), + ) + return + } + + if found == nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to locate any schedule with the name: %s", searchName), + "", + ) + return + } + + model := dataSourceScheduleModel{ + ID: types.StringValue(found.ID), + Name: types.StringValue(found.Name), + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +type dataSourceScheduleModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} diff --git a/pagerduty/data_source_pagerduty_schedule_test.go b/pagerdutyplugin/data_source_pagerduty_schedule_test.go similarity index 87% rename from pagerduty/data_source_pagerduty_schedule_test.go rename to pagerdutyplugin/data_source_pagerduty_schedule_test.go index 7abdd1b76..e194ce9db 100644 --- a/pagerduty/data_source_pagerduty_schedule_test.go +++ b/pagerdutyplugin/data_source_pagerduty_schedule_test.go @@ -15,12 +15,13 @@ func TestAccDataSourcePagerDutySchedule_Basic(t *testing.T) { email := fmt.Sprintf("%s@foo.test", username) schedule := fmt.Sprintf("tf-%s", acctest.RandString(5)) location := "Europe/Berlin" - start := timeNowInLoc(location).Add(24 * time.Hour).Round(1 * time.Hour).Format(time.RFC3339) - rotationVirtualStart := timeNowInLoc(location).Add(24 * time.Hour).Round(1 * time.Hour).Format(time.RFC3339) + start := testAccTimeNow().Add(24 * time.Hour).Round(1 * time.Hour).Format(time.RFC3339) + rotationVirtualStart := testAccTimeNow().Add(24 * time.Hour).Round(1 * time.Hour).Format(time.RFC3339) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + Steps: []resource.TestStep{ { Config: testAccDataSourcePagerDutyScheduleConfig(username, email, schedule, location, start, rotationVirtualStart), From b560686346f10ab3a9d8757da069dcfbbb85c51f Mon Sep 17 00:00:00 2001 From: Carlos Gajardo Date: Tue, 26 Mar 2024 16:03:48 -0300 Subject: [PATCH 8/8] Migrate resource team membership --- pagerduty/provider.go | 1 + pagerduty/provider_test.go | 19 +- .../resource_pagerduty_team_membership.go | 1 + .../import_pagerduty_team_membership_test.go | 12 +- pagerdutyplugin/provider.go | 5 + .../resource_pagerduty_team_membership.go | 368 ++++++++++++++++++ ...resource_pagerduty_team_membership_test.go | 77 ++-- 7 files changed, 429 insertions(+), 54 deletions(-) rename {pagerduty => pagerdutyplugin}/import_pagerduty_team_membership_test.go (74%) create mode 100644 pagerdutyplugin/resource_pagerduty_team_membership.go rename {pagerduty => pagerdutyplugin}/resource_pagerduty_team_membership_test.go (73%) diff --git a/pagerduty/provider.go b/pagerduty/provider.go index 7281c95bd..df1ab6960 100644 --- a/pagerduty/provider.go +++ b/pagerduty/provider.go @@ -160,6 +160,7 @@ func Provider(isMux bool) *schema.Provider { delete(p.ResourcesMap, "pagerduty_addon") delete(p.ResourcesMap, "pagerduty_business_service") delete(p.ResourcesMap, "pagerduty_team") + delete(p.ResourcesMap, "pagerduty_team_membership") } p.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { diff --git a/pagerduty/provider_test.go b/pagerduty/provider_test.go index 57118dd9b..81fff861b 100644 --- a/pagerduty/provider_test.go +++ b/pagerduty/provider_test.go @@ -351,9 +351,26 @@ func testAccCheckPagerDutyTeamDestroy(s *terraform.State) error { func testAccCheckPagerDutyTeamConfig(team string) string { return fmt.Sprintf(` - resource "pagerduty_team" "foo" { name = "%s" description = "foo" }`, team) } + +func testAccCheckPagerDutyTeamMembershipDestroy(s *terraform.State) error { + client, _ := testAccProvider.Meta().(*Config).Client() + for _, r := range s.RootModule().Resources { + if r.Type != "pagerduty_team_membership" { + continue + } + + user, _, err := client.Users.Get(r.Primary.Attributes["user_id"], &pagerduty.GetUserOptions{}) + if err == nil { + if isTeamMember(user, r.Primary.Attributes["team_id"]) { + return fmt.Errorf("%s is still a member of: %s", user.ID, r.Primary.Attributes["team_id"]) + } + } + } + + return nil +} diff --git a/pagerduty/resource_pagerduty_team_membership.go b/pagerduty/resource_pagerduty_team_membership.go index 70456cd4e..e7033d089 100644 --- a/pagerduty/resource_pagerduty_team_membership.go +++ b/pagerduty/resource_pagerduty_team_membership.go @@ -12,6 +12,7 @@ import ( "github.com/heimweh/go-pagerduty/pagerduty" ) +// Deprecated: Migrated to pagerdutyplugin.resourceTeamMembership. Kept for testing purposes. func resourcePagerDutyTeamMembership() *schema.Resource { return &schema.Resource{ Create: resourcePagerDutyTeamMembershipCreate, diff --git a/pagerduty/import_pagerduty_team_membership_test.go b/pagerdutyplugin/import_pagerduty_team_membership_test.go similarity index 74% rename from pagerduty/import_pagerduty_team_membership_test.go rename to pagerdutyplugin/import_pagerduty_team_membership_test.go index d290db143..07ed3ac88 100644 --- a/pagerduty/import_pagerduty_team_membership_test.go +++ b/pagerdutyplugin/import_pagerduty_team_membership_test.go @@ -13,9 +13,9 @@ func TestAccPagerDutyTeamMembership_import(t *testing.T) { team := fmt.Sprintf("tf-%s", acctest.RandString(5)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTeamMembershipConfig(user, team), @@ -36,9 +36,9 @@ func TestAccPagerDutyTeamMembership_importWithRole(t *testing.T) { role := "manager" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTeamMembershipWithRoleConfig(user, team, role), diff --git a/pagerdutyplugin/provider.go b/pagerdutyplugin/provider.go index 6f480c5ef..91d0125f1 100644 --- a/pagerdutyplugin/provider.go +++ b/pagerdutyplugin/provider.go @@ -79,6 +79,7 @@ func (p *Provider) Resources(_ context.Context) [](func() resource.Resource) { func() resource.Resource { return &resourceServiceDependency{} }, func() resource.Resource { return &resourceTagAssignment{} }, func() resource.Resource { return &resourceTag{} }, + func() resource.Resource { return &resourceTeamMembership{} }, func() resource.Resource { return &resourceTeam{} }, func() resource.Resource { return &resourceUserHandoffNotificationRule{} }, func() resource.Resource { return &resourceUserNotificationRule{} }, @@ -210,6 +211,7 @@ type providerArguments struct { } type SchemaGetter interface { + Get(context.Context, interface{}) diag.Diagnostics GetAttribute(context.Context, path.Path, interface{}) diag.Diagnostics } @@ -219,3 +221,6 @@ func extractString(ctx context.Context, schema SchemaGetter, name string, diags diags.Append(d...) return s.ValueStringPointer() } + +// Helper constant used to have a semantically meaningful value in function calls +const RetryNotFound = true diff --git a/pagerdutyplugin/resource_pagerduty_team_membership.go b/pagerdutyplugin/resource_pagerduty_team_membership.go new file mode 100644 index 000000000..032e97ebc --- /dev/null +++ b/pagerdutyplugin/resource_pagerduty_team_membership.go @@ -0,0 +1,368 @@ +package pagerduty + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "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/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" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +type resourceTeamMembership struct{ client *pagerduty.Client } + +var ( + _ resource.ResourceWithConfigure = (*resourceTeamMembership)(nil) + _ resource.ResourceWithImportState = (*resourceTeamMembership)(nil) +) + +func (r *resourceTeamMembership) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "pagerduty_team_membership" +} + +func (r *resourceTeamMembership) 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(), + }, + }, + "user_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "team_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("observer", "responder", "manager"), + }, + Default: stringdefault.StaticString("manager"), + }, + }, + } +} + +func (r *resourceTeamMembership) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + model := requestAddTeamMembership(ctx, r.client, req.Plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + id := model.ID.ValueString() + role := model.Role.ValueString() + + model, err := requestGetTeamMembership(ctx, r.client, id, &role, RetryNotFound, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error creating PagerDuty team membership %s", model.ID), + err.Error(), + ) + return + } + + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + +func (r *resourceTeamMembership) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var id types.String + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &id)...) + if resp.Diagnostics.HasError() { + return + } + log.Printf("[INFO] Reading PagerDuty team membership %s", id) + + state, err := requestGetTeamMembership(ctx, r.client, id.ValueString(), nil, !RetryNotFound, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading PagerDuty team membership %s", id), + err.Error(), + ) + return + } + + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceTeamMembership) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + model := requestAddTeamMembership(ctx, r.client, req.Plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + id := model.ID.ValueString() + role := model.Role.ValueString() + + model, err := requestGetTeamMembership(ctx, r.client, id, &role, RetryNotFound, &resp.Diagnostics) + if err != nil { + if util.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating PagerDuty team membership %s", model.ID), + err.Error(), + ) + return + } + + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + +func (r *resourceTeamMembership) 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 team membership %s", id) + + userID, teamID, err := util.ResourcePagerDutyParseColonCompoundID(id.ValueString()) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Invalid Team Membership ID %s", id), err.Error()) + return + } + + userIsInEP := false + err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + if err := r.client.RemoveUserFromTeamWithContext(ctx, teamID, userID); err != nil { + userIsInEP = strings.Contains(err.Error(), "User cannot be removed as they belong to an escalation policy on this team") + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + return nil + }) + if userIsInEP { + eps := fetchEscalationPoliciesWithUser(ctx, r.client, userID, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + diagnoseEscalationPoliciesAssociatedToUser(userID, teamID, eps, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + } + if err != nil && !util.IsNotFoundError(err) { + resp.Diagnostics.AddError( + fmt.Sprintf("Error deleting PagerDuty team membership %s", id), + err.Error(), + ) + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *resourceTeamMembership) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(ConfigurePagerdutyClient(&r.client, req.ProviderData)...) +} + +func (r *resourceTeamMembership) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +type resourceTeamMembershipModel struct { + ID types.String `tfsdk:"id"` + TeamID types.String `tfsdk:"team_id"` + UserID types.String `tfsdk:"user_id"` + Role types.String `tfsdk:"role"` +} + +func requestGetTeamMembership(ctx context.Context, client *pagerduty.Client, id string, neededRole *string, retryNotFound bool, diags *diag.Diagnostics) (resourceTeamMembershipModel, error) { + var model resourceTeamMembershipModel + + userID, teamID, err := util.ResourcePagerDutyParseColonCompoundID(id) + if err != nil { + diags.AddError(fmt.Sprintf("Invalid Team Membership ID %s", id), err.Error()) + return model, nil + } + + err = retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + resp, err := client.ListTeamMembers(ctx, teamID, pagerduty.ListTeamMembersOptions{Limit: 100}) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + if !retryNotFound && util.IsNotFoundError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + + for _, m := range resp.Members { + if m.User.ID == userID { + if neededRole != nil && m.Role != *neededRole { + err = fmt.Errorf("Role %q fetched is different from configuration %q", m.Role, *neededRole) + return retry.RetryableError(err) + } + model = flattenTeamMembership(userID, teamID, m.Role) + return nil + } + } + + err = pagerduty.APIError{StatusCode: http.StatusNotFound} + if retryNotFound { + return retry.RetryableError(err) + } + return retry.NonRetryableError(err) + }) + + return model, err +} + +func requestAddTeamMembership(ctx context.Context, client *pagerduty.Client, plan SchemaGetter, diags *diag.Diagnostics) resourceTeamMembershipModel { + var model resourceTeamMembershipModel + + diags.Append(plan.Get(ctx, &model)...) + if diags.HasError() { + return model + } + opts, _ := buildPagerdutyTeamMembership(&model) + log.Printf("[INFO] Creating PagerDuty team membership for user %s at team %s using role %s", opts.UserID, opts.TeamID, opts.Role) + + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + err := client.AddUserToTeamWithContext(ctx, opts) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + model.ID = flattenTeamMembershipID(opts.UserID, opts.TeamID) + return nil + }) + if err != nil { + diags.AddError( + fmt.Sprintf("Error creating PagerDuty team membership for user %s at team %s using role %s", opts.UserID, opts.TeamID, opts.Role), + err.Error(), + ) + return model + } + return model +} + +func fetchEscalationPoliciesWithUser(ctx context.Context, client *pagerduty.Client, userID string, diags *diag.Diagnostics) []pagerduty.EscalationPolicy { + var oncalls []pagerduty.OnCall + err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + resp, err := client.ListOnCallsWithContext(ctx, pagerduty.ListOnCallOptions{UserIDs: []string{userID}, Limit: 100}) + if err != nil { + if util.IsBadRequestError(err) { + return retry.NonRetryableError(err) + } + return retry.RetryableError(err) + } + oncalls = resp.OnCalls + return nil + }) + if err != nil { + diags.AddError( + fmt.Sprintf("Error reading escalation policies for PagerDuty user %s", userID), + err.Error(), + ) + return nil + } + + var eps []pagerduty.EscalationPolicy + for _, oc := range oncalls { + eps = append(eps, oc.EscalationPolicy) + } + + return eps +} + +func diagnoseEscalationPoliciesAssociatedToUser(userID, teamID string, eps []pagerduty.EscalationPolicy, diags *diag.Diagnostics) { + if len(eps) == 0 { + return // No diagnostics + } + + pdURL, err := url.Parse(eps[0].HTMLURL) + if err != nil { + return // No diagnostics + } + + var links []string + for _, ep := range eps { + links = append(links, fmt.Sprintf("\t* %s", ep.HTMLURL)) + } + + diags.AddError( + fmt.Sprintf("User %q can't be removed from Team %q", userID, teamID), + fmt.Sprintf(`As the user belongs to an Escalation Policy on this team. Please take one of the following remediation measures in order to unblock the Team Membership removal: +1. Remove the user from the following Escalation Policies: +%s +2. Remove the Escalation Policies from the Team: + https://%s/teams/%s + +After completing one of the above given remediation options come back to continue with the destruction of Team Membership.`, + strings.Join(links, "\n"), + pdURL.Hostname(), + teamID, + ), + ) +} + +func buildPagerdutyTeamMembership(model *resourceTeamMembershipModel) (pagerduty.AddUserToTeamOptions, string) { + opts := pagerduty.AddUserToTeamOptions{ + TeamID: model.TeamID.ValueString(), + UserID: model.UserID.ValueString(), + Role: pagerduty.TeamUserRole(model.Role.ValueString()), + } + return opts, model.ID.ValueString() +} + +func flattenTeamMembership(userID, teamID, role string) resourceTeamMembershipModel { + model := resourceTeamMembershipModel{ + ID: flattenTeamMembershipID(userID, teamID), + UserID: types.StringValue(userID), + TeamID: types.StringValue(teamID), + Role: types.StringValue(role), + } + return model +} + +func flattenTeamMembershipID(userID, teamID string) types.String { + return types.StringValue(fmt.Sprintf("%s:%s", userID, teamID)) +} diff --git a/pagerduty/resource_pagerduty_team_membership_test.go b/pagerdutyplugin/resource_pagerduty_team_membership_test.go similarity index 73% rename from pagerduty/resource_pagerduty_team_membership_test.go rename to pagerdutyplugin/resource_pagerduty_team_membership_test.go index cc6b26d27..71fb73338 100644 --- a/pagerduty/resource_pagerduty_team_membership_test.go +++ b/pagerdutyplugin/resource_pagerduty_team_membership_test.go @@ -1,13 +1,14 @@ package pagerduty import ( + "context" "fmt" "testing" + "github.com/PagerDuty/go-pagerduty" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/heimweh/go-pagerduty/pagerduty" ) func TestAccPagerDutyTeamMembership_Basic(t *testing.T) { @@ -15,9 +16,9 @@ func TestAccPagerDutyTeamMembership_Basic(t *testing.T) { team := fmt.Sprintf("tf-%s", acctest.RandString(5)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTeamMembershipConfig(user, team), @@ -35,9 +36,9 @@ func TestAccPagerDutyTeamMembership_WithRole(t *testing.T) { role := "manager" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTeamMembershipWithRoleConfig(user, team, role), @@ -56,9 +57,9 @@ func TestAccPagerDutyTeamMembership_WithRoleConsistentlyAssigned(t *testing.T) { secondRole := "responder" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(), + CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, Steps: []resource.TestStep{ { Config: testAccCheckPagerDutyTeamMembershipWithRoleConfig(user, team, firstRole), @@ -80,43 +81,16 @@ func TestAccPagerDutyTeamMembership_WithRoleConsistentlyAssigned(t *testing.T) { }) } -func TestAccPagerDutyTeamMembership_DestroyWithEscalationPolicyDependant(t *testing.T) { - user := fmt.Sprintf("tf-%s", acctest.RandString(5)) - team := fmt.Sprintf("tf-%s", acctest.RandString(5)) - role := "manager" - escalationPolicy := fmt.Sprintf("tf-%s", acctest.RandString(5)) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPagerDutyTeamMembershipDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCheckPagerDutyTeamMembershipDestroyWithEscalationPolicyDependant(user, team, role, escalationPolicy), - Check: resource.ComposeTestCheckFunc( - testAccCheckPagerDutyTeamMembershipExists("pagerduty_team_membership.foo"), - ), - }, - { - Config: testAccCheckPagerDutyTeamMembershipDestroyWithEscalationPolicyDependantUpdated(user, team, role, escalationPolicy), - Check: resource.ComposeTestCheckFunc( - testAccCheckPagerDutyTeamMembershipNoExists("pagerduty_team_membership.foo"), - ), - }, - }, - }) -} - func testAccCheckPagerDutyTeamMembershipDestroy(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() for _, r := range s.RootModule().Resources { if r.Type != "pagerduty_team_membership" { continue } - user, _, err := client.Users.Get(r.Primary.Attributes["user_id"], &pagerduty.GetUserOptions{}) + ctx := context.Background() + user, err := testAccProvider.client.GetUserWithContext(ctx, r.Primary.Attributes["user_id"], pagerduty.GetUserOptions{}) if err == nil { - if isTeamMember(user, r.Primary.Attributes["team_id"]) { + if helperIsTeamMember(user, r.Primary.Attributes["team_id"]) { return fmt.Errorf("%s is still a member of: %s", user.ID, r.Primary.Attributes["team_id"]) } } @@ -127,7 +101,6 @@ func testAccCheckPagerDutyTeamMembershipDestroy(s *terraform.State) error { func testAccCheckPagerDutyTeamMembershipExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() rs, ok := s.RootModule().Resources[n] if !ok { @@ -142,16 +115,17 @@ func testAccCheckPagerDutyTeamMembershipExists(n string) resource.TestCheckFunc teamID := rs.Primary.Attributes["team_id"] role := rs.Primary.Attributes["role"] - user, _, err := client.Users.Get(userID, &pagerduty.GetUserOptions{}) + ctx := context.Background() + user, err := testAccProvider.client.GetUserWithContext(ctx, userID, pagerduty.GetUserOptions{}) if err != nil { return err } - if !isTeamMember(user, teamID) { + if !helperIsTeamMember(user, teamID) { return fmt.Errorf("%s is not a member of: %s", userID, teamID) } - resp, _, err := client.Teams.GetMembers(teamID, &pagerduty.GetMembersOptions{}) + resp, err := testAccProvider.client.ListTeamMembers(ctx, teamID, pagerduty.ListTeamMembersOptions{}) if err != nil { return err } @@ -170,7 +144,6 @@ func testAccCheckPagerDutyTeamMembershipExists(n string) resource.TestCheckFunc func testAccCheckPagerDutyTeamMembershipNoExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { - client, _ := testAccProvider.Meta().(*Config).Client() rs, ok := s.RootModule().Resources[n] if !ok { @@ -184,12 +157,13 @@ func testAccCheckPagerDutyTeamMembershipNoExists(n string) resource.TestCheckFun userID := rs.Primary.Attributes["user_id"] teamID := rs.Primary.Attributes["team_id"] - user, _, err := client.Users.Get(userID, &pagerduty.GetUserOptions{}) + ctx := context.Background() + user, err := testAccProvider.client.GetUserWithContext(ctx, userID, pagerduty.GetUserOptions{}) if err != nil { return err } - if isTeamMember(user, teamID) { + if helperIsTeamMember(user, teamID) { return fmt.Errorf("%s is still a member of: %s", userID, teamID) } @@ -197,6 +171,15 @@ func testAccCheckPagerDutyTeamMembershipNoExists(n string) resource.TestCheckFun } } +func helperIsTeamMember(user *pagerduty.User, teamID string) bool { + for _, team := range user.Teams { + if teamID == team.ID { + return true + } + } + return false +} + func testAccCheckPagerDutyTeamMembershipConfig(user, team string) string { return fmt.Sprintf(` resource "pagerduty_user" "foo" {