From c237c0bb5729a3d045099f56d6922ad92b5891c7 Mon Sep 17 00:00:00 2001 From: vagrawal-newrelic Date: Thu, 26 Sep 2024 00:59:12 +0530 Subject: [PATCH] feat(key_transaction): add new resource to manage key transactions (#2748) --- go.mod | 2 +- go.sum | 4 +- newrelic/provider.go | 1 + newrelic/resource_newrelic_key_transaction.go | 207 ++++++++++++++++++ .../resource_newrelic_key_transaction_test.go | 103 +++++++++ website/docs/r/key_transaction.html.markdown | 51 +++++ 6 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 newrelic/resource_newrelic_key_transaction.go create mode 100644 newrelic/resource_newrelic_key_transaction_test.go create mode 100644 website/docs/r/key_transaction.html.markdown diff --git a/go.mod b/go.mod index 2649f915c..669b6a630 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/newrelic/go-agent/v3 v3.30.0 github.com/newrelic/go-insights v1.0.3 - github.com/newrelic/newrelic-client-go/v2 v2.46.0 + github.com/newrelic/newrelic-client-go/v2 v2.47.0 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 ) diff --git a/go.sum b/go.sum index 99fb0c6b7..0e6ce6263 100644 --- a/go.sum +++ b/go.sum @@ -270,8 +270,8 @@ github.com/newrelic/go-agent/v3 v3.30.0 h1:ZXHCT/Cot4iIPwcegCZURuRQOsfmGA6wilW+S github.com/newrelic/go-agent/v3 v3.30.0/go.mod h1:9utrgxlSryNqRrTvII2XBL+0lpofXbqXApvVWPpbzUg= github.com/newrelic/go-insights v1.0.3 h1:zSNp1CEZnXktzSIEsbHJk8v6ZihdPFP2WsO/fzau3OQ= github.com/newrelic/go-insights v1.0.3/go.mod h1:A20BoT8TNkqPGX2nS/Z2fYmKl3Cqa3iKZd4whzedCY4= -github.com/newrelic/newrelic-client-go/v2 v2.46.0 h1:J1dKQFRKfQJQQFbP4EQGRs6JsYL20gXJxRmTjXLuB9E= -github.com/newrelic/newrelic-client-go/v2 v2.46.0/go.mod h1:pDFY24/6iIMEbPIdowTRrRn9YYwkXc3j+B+XpTb4oF4= +github.com/newrelic/newrelic-client-go/v2 v2.47.0 h1:4+Q4ynp1lHm2t8OopQ6lmac+dJD0E3AvbHQAuSW7+ws= +github.com/newrelic/newrelic-client-go/v2 v2.47.0/go.mod h1:pDFY24/6iIMEbPIdowTRrRn9YYwkXc3j+B+XpTb4oF4= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= diff --git a/newrelic/provider.go b/newrelic/provider.go index dfeecd2f6..c2a5c6f72 100755 --- a/newrelic/provider.go +++ b/newrelic/provider.go @@ -162,6 +162,7 @@ func Provider() *schema.Provider { "newrelic_group": resourceNewRelicGroup(), "newrelic_infra_alert_condition": resourceNewRelicInfraAlertCondition(), "newrelic_insights_event": resourceNewRelicInsightsEvent(), + "newrelic_key_transaction": resourceNewRelicKeyTransaction(), "newrelic_log_parsing_rule": resourceNewRelicLogParsingRule(), "newrelic_monitor_downtime": resourceNewRelicMonitorDowntime(), "newrelic_notification_channel": resourceNewRelicNotificationChannel(), diff --git a/newrelic/resource_newrelic_key_transaction.go b/newrelic/resource_newrelic_key_transaction.go new file mode 100644 index 000000000..4badbeb8a --- /dev/null +++ b/newrelic/resource_newrelic_key_transaction.go @@ -0,0 +1,207 @@ +package newrelic + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/newrelic/newrelic-client-go/v2/pkg/common" + "github.com/newrelic/newrelic-client-go/v2/pkg/entities" + "github.com/newrelic/newrelic-client-go/v2/pkg/keytransaction" +) + +func resourceNewRelicKeyTransaction() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceNewRelicKeyTransactionCreate, + ReadContext: resourceNewRelicKeyTransactionRead, + UpdateContext: resourceNewRelicKeyTransactionUpdate, + DeleteContext: resourceNewRelicKeyTransactionDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "apdex_index": { + Type: schema.TypeFloat, + Description: "The acceptable amount of the time spent in the backend before customers get frustrated (Apdex target)", + Required: true, + }, + "application_guid": { + Type: schema.TypeString, + Description: "The GUID of the application.", + Required: true, + ForceNew: true, + }, + "browser_apdex_target": { + Type: schema.TypeFloat, + Description: "The acceptable amount of time for rendering a page in a browser before customers get frustrated (browser Apdex target).", + Required: true, + }, + "metric_name": { + Type: schema.TypeString, + Description: "The name of the metric underlying this key transaction", + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Description: "The name of the key transaction.", + Required: true, + }, + "domain": { + Type: schema.TypeString, + Description: "Domain of the entity.", + Required: false, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Description: "Type of the entity.", + Required: false, + Computed: true, + }, + }, + } +} + +func resourceNewRelicKeyTransactionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*ProviderConfig).NewClient + + apdexIndex, + applicationGUID, + browserApdexTarget, + metricName, + name := resourceNewRelicKeyTransactionFetchValuesFromConfig(d) + + log.Printf("[INFO] Creating New Relic Key Transaction %s", name) + createKeyTransactionResult, err := client.KeyTransaction.KeyTransactionCreate( + apdexIndex, + keytransaction.EntityGUID(applicationGUID), + browserApdexTarget, + metricName, + name, + ) + + if err != nil { + return diag.FromErr(err) + } + if createKeyTransactionResult == nil { + return diag.FromErr(fmt.Errorf("something went wrong while creating the key transaction")) + } + + resourceNewRelicKeyTransactionSetValuesToState(d, *createKeyTransactionResult, keytransaction.KeyTransactionUpdateResult{}) + + return nil +} + +func resourceNewRelicKeyTransactionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + providerConfig := meta.(*ProviderConfig) + client := providerConfig.NewClient + + guid := d.Id() + + resp, err := client.Entities.GetEntityWithContext(ctx, common.EntityGUID(guid)) + if err != nil { + return diag.FromErr(err) + } + + if resp == nil { + d.SetId("") + return diag.FromErr(fmt.Errorf("no key transaction found with given guid %s", guid)) + } + + switch (*resp).(type) { + case *entities.KeyTransactionEntity: + entity := (*resp).(*entities.KeyTransactionEntity) + _ = d.Set("apdex_index", entity.ApdexTarget) + _ = d.Set("application_guid", entity.Application.GUID) + _ = d.Set("browser_apdex_target", entity.BrowserApdexTarget) + _ = d.Set("metric_name", entity.MetricName) + _ = d.Set("name", entity.Name) + default: + return diag.FromErr(fmt.Errorf("entity with GUID %s was not a key transaction", guid)) + } + return nil +} + +func resourceNewRelicKeyTransactionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*ProviderConfig).NewClient + + apdexIndex, + _, + browserApdexTarget, + _, + name := resourceNewRelicKeyTransactionFetchValuesFromConfig(d) + + guid := keytransaction.EntityGUID(d.Id()) + log.Printf("[INFO] Updating New Relic Key Transaction %s", name) + + updateKeyTransactionResult, err := client.KeyTransaction.KeyTransactionUpdate(apdexIndex, browserApdexTarget, guid, name) + + if err != nil { + + return diag.FromErr(err) + } + if updateKeyTransactionResult == nil { + return diag.FromErr(fmt.Errorf("something went wrong while updating the key transaction")) + } + + resourceNewRelicKeyTransactionSetValuesToState(d, keytransaction.KeyTransactionCreateResult{}, *updateKeyTransactionResult) + return nil +} + +func resourceNewRelicKeyTransactionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*ProviderConfig).NewClient + keyTransactionGUID := keytransaction.EntityGUID(d.Id()) + + log.Printf("[INFO] Deleting New Relic Key Transaction %s", d.Id()) + _, err := client.KeyTransaction.KeyTransactionDelete(keyTransactionGUID) + + if err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceNewRelicKeyTransactionFetchValuesFromConfig(d *schema.ResourceData) ( + apdexIndex float64, + applicationGUID string, + browserApdexTarget float64, + metricName string, + name string, +) { + apdexIndex = d.Get("apdex_index").(float64) + applicationGUID = d.Get("application_guid").(string) + browserApdexTarget = d.Get("browser_apdex_target").(float64) + metricName = d.Get("metric_name").(string) + name = d.Get("name").(string) + + return apdexIndex, applicationGUID, browserApdexTarget, metricName, name +} + +func resourceNewRelicKeyTransactionSetValuesToState( + d *schema.ResourceData, + createKeyTransactionResult keytransaction.KeyTransactionCreateResult, + updateKeyTransactionResult keytransaction.KeyTransactionUpdateResult, +) { + if createKeyTransactionResult.GUID != "" && updateKeyTransactionResult.Name == "" { + d.SetId(string(createKeyTransactionResult.GUID)) + _ = d.Set("apdex_index", createKeyTransactionResult.ApdexTarget) + _ = d.Set("application_guid", createKeyTransactionResult.Application.GUID) + _ = d.Set("browser_apdex_target", createKeyTransactionResult.BrowserApdexTarget) + _ = d.Set("metric_name", createKeyTransactionResult.MetricName) + _ = d.Set("name", createKeyTransactionResult.Name) + entity := createKeyTransactionResult.Application.Entity.(*keytransaction.ApmApplicationEntityOutline) + _ = d.Set("domain", entity.Domain) + _ = d.Set("type", entity.Type) + } else if createKeyTransactionResult.GUID == "" && updateKeyTransactionResult.Name != "" { + _ = d.Set("apdex_index", updateKeyTransactionResult.ApdexTarget) + _ = d.Set("application_guid", updateKeyTransactionResult.Application.GUID) + _ = d.Set("browser_apdex_target", updateKeyTransactionResult.BrowserApdexTarget) + _ = d.Set("name", updateKeyTransactionResult.Name) + entity := updateKeyTransactionResult.Application.Entity.(*keytransaction.ApmApplicationEntityOutline) + _ = d.Set("domain", entity.Domain) + _ = d.Set("type", entity.Type) + } +} diff --git a/newrelic/resource_newrelic_key_transaction_test.go b/newrelic/resource_newrelic_key_transaction_test.go new file mode 100644 index 000000000..5b8cff7d3 --- /dev/null +++ b/newrelic/resource_newrelic_key_transaction_test.go @@ -0,0 +1,103 @@ +//go:build integration +// +build integration + +package newrelic + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/newrelic/newrelic-client-go/v2/pkg/common" + "github.com/newrelic/newrelic-client-go/v2/pkg/entities" +) + +func TestAccNewRelicKeyTransaction_Basic(t *testing.T) { + randomName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + resourceName := "newrelic_key_transaction.foo" + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create + { + Config: testAccNewRelicKeyTransactionBasicConfiguration(fmt.Sprintf("%s", randomName)), + Check: resource.ComposeTestCheckFunc( + testAccNewRelicCheckKeyTransactionExists(resourceName), + ), + }, + // Update + { + Config: testAccNewRelicKeyTransactionBasicConfiguration(fmt.Sprintf("%s-updated", randomName)), + Check: resource.ComposeTestCheckFunc( + testAccNewRelicCheckKeyTransactionExists(resourceName), + ), + }, + // Import + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "domain", + "type", + }, + }, + }, + }) +} + +func TestAccNewRelicKeyTransaction_DuplicateNameError(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + // create a key transaction with a name that already exists in the UI + // this is expected to throw an error, as only one key transaction may be created per metric name + { + Config: testAccNewRelicKeyTransactionBasicConfiguration(fmt.Sprintf("%s", "terraform_acceptance_test_key_transaction_donot_delete")), + ExpectError: regexp.MustCompile("\\s*"), + }, + }, + }) +} + +func testAccNewRelicKeyTransactionBasicConfiguration(name string) string { + return fmt.Sprintf(` + resource "newrelic_key_transaction" "foo" { + apdex_index = 0.5 + application_guid = "MzgwNjUyNnxBUE18QVBQTElDQVRJT058NTUzNDQ4MjAy" + browser_apdex_target = 0.5 + metric_name = "test" + name = "%[1]s" + } + `, name) +} + +func testAccNewRelicCheckKeyTransactionExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no key transaction ID is set") + } + + client := testAccProvider.Meta().(*ProviderConfig).NewClient + + found, err := client.Entities.GetEntity(common.EntityGUID(rs.Primary.ID)) + if err != nil { + return fmt.Errorf(err.Error()) + } + + x := (*found).(*entities.KeyTransactionEntity) + if x.GUID != common.EntityGUID(rs.Primary.ID) { + return fmt.Errorf("key transaction not found") + } + + return nil + } +} diff --git a/website/docs/r/key_transaction.html.markdown b/website/docs/r/key_transaction.html.markdown new file mode 100644 index 000000000..4e8560808 --- /dev/null +++ b/website/docs/r/key_transaction.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "newrelic" +page_title: "New Relic: newrelic_key_transaction" +sidebar_current: "docs-newrelic-resource-key-transaction" +description: |- + Create a new New Relic Key Transaction. +--- + +# Resource: newrelic\_key\_transaction + +Use this resource to create a new Key Transaction in New Relic. + +-> **NOTE:** For more information on Key Transactions, head over to [this page](https://docs.newrelic.com/docs/apm/transactions/key-transactions/introduction-key-transactions/) in New Relic's docs. + +## Example Usage + +```hcl +resource "newrelic_key_transaction" "foo" { + application_guid = "MzgfNjUyNnxBUE19QVBQTElDQVHJT068NTUfNDT4MjUy" + apdex_index = 0.5 + browser_apdex_target = 0.5 + metric_name = "WebTransaction/Function/__main__:foo_bar" + name = "Sample Key Transaction" +} +``` +## Argument Reference + +The following arguments are supported by this resource. + +* `application_guid` - (Required) The GUID of the APM Application comprising transactions, of which one would be made a key transaction. +* `metric_name` - (Required) - The name of the underlying metric monitored by the key transaction to be created. +* `name` - (Required) - The name of the key transaction. +* `apdex_index` - (Required) A decimal value, measuring user satisfaction with response times, ranging from 0 (frustrated) to 1 (satisfied). +* `browser_apdex_target` - (Required) A decimal value representing the response time threshold for satisfactory experience (e.g., 0.5 seconds). + +-> **NOTE:** It may be noted that the `metric_name` and `application_guid` of a Key Transaction _cannot_ be updated in a key transaction that has already been created; since this is not supported. As a consequence, altering the values of `application_guid` and/or `metric_name` of a `newrelic_key_transaction` resource created (to try updating these values) would result in `terraform plan` prompting a forced destruction and re-creation of the resource. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported by this resource. + +* `id` - The GUID of the created key transaction. +* `domain` - The domain of the entity monitored by the key transaction. +* `type` - The type of the entity monitored by the key transaction. + +## Import +A Key Transaction in New Relic may be imported into Terraform using its GUID specified in the `` field, in the following command. + +```bash +$ terraform import newrelic_key_transaction.foo +```