Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(entity_tags): add an entity tag resource #679

Merged
merged 5 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/deps.mk
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ deps: tools deps-only

deps-only:
@echo "=== $(PROJECT_NAME) === [ deps ]: Installing package dependencies required by the project..."
@$(GO) mod tidy
#@$(GO) mod tidy
sanderblue marked this conversation as resolved.
Show resolved Hide resolved
@$(GO) mod download

tools: check-version
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module github.com/terraform-providers/terraform-provider-newrelic
go 1.13

require (
github.com/Azure/go-autorest v14.0.1+incompatible // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 // indirect
github.com/bflad/tfproviderlint v0.14.0
github.com/client9/misspell v0.3.4
github.com/golangci/golangci-lint v1.27.0
github.com/hashicorp/terraform v0.12.26 // indirect
github.com/hashicorp/terraform-plugin-sdk v1.10.0
github.com/newrelic/go-agent/v3 v3.6.0
github.com/newrelic/go-insights v1.0.3
Expand Down
180 changes: 163 additions & 17 deletions go.sum

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions newrelic/data_source_newrelic_entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package newrelic
import (
"fmt"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
Expand All @@ -15,9 +14,6 @@ func TestAccNewRelicEntityData_Basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)

// We need to give the entity search engine time to index the app
time.Sleep(5 * time.Second)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
Expand Down
10 changes: 10 additions & 0 deletions newrelic/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,13 @@ func envAccountID() (interface{}, error) {

return nil, nil
}

func stringInSlice(slice []string, str string) bool {
for _, s := range slice {
if str == s {
return true
}
}

return false
}
13 changes: 7 additions & 6 deletions newrelic/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/hashicorp/terraform-plugin-sdk/httpclient"
"github.com/hashicorp/terraform-plugin-sdk/meta"
"github.com/hashicorp/terraform-plugin-sdk/terraform"

"github.com/terraform-providers/terraform-provider-newrelic/version"
Expand Down Expand Up @@ -129,18 +129,19 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"newrelic_alert_channel": resourceNewRelicAlertChannel(),
"newrelic_alert_condition": resourceNewRelicAlertCondition(),
"newrelic_alert_policy_channel": resourceNewRelicAlertPolicyChannel(),
"newrelic_alert_policy": resourceNewRelicAlertPolicy(),
"newrelic_alert_policy_channel": resourceNewRelicAlertPolicyChannel(),
"newrelic_application_settings": resourceNewRelicApplicationSettings(),
"newrelic_plugins_alert_condition": resourceNewRelicPluginsAlertCondition(),
"newrelic_dashboard": resourceNewRelicDashboard(),
"newrelic_entity_tags": resourceNewRelicEntityTags(),
"newrelic_infra_alert_condition": resourceNewRelicInfraAlertCondition(),
"newrelic_insights_event": resourceNewRelicInsightsEvent(),
"newrelic_nrql_alert_condition": resourceNewRelicNrqlAlertCondition(),
"newrelic_plugins_alert_condition": resourceNewRelicPluginsAlertCondition(),
"newrelic_synthetics_alert_condition": resourceNewRelicSyntheticsAlertCondition(),
"newrelic_synthetics_label": resourceNewRelicSyntheticsLabel(),
"newrelic_synthetics_monitor": resourceNewRelicSyntheticsMonitor(),
"newrelic_synthetics_monitor_script": resourceNewRelicSyntheticsMonitorScript(),
"newrelic_synthetics_label": resourceNewRelicSyntheticsLabel(),
"newrelic_synthetics_secure_credential": resourceNewRelicSyntheticsSecureCredential(),
"newrelic_workload": resourceNewRelicWorkload(),
},
Expand All @@ -161,8 +162,8 @@ func Provider() terraform.ResourceProvider {
func providerConfigure(data *schema.ResourceData, terraformVersion string) (interface{}, error) {
adminAPIKey := data.Get("admin_api_key").(string)
personalAPIKey := data.Get("api_key").(string)
userAgent := fmt.Sprintf("%s %s/%s", httpclient.TerraformUserAgent(terraformVersion), TerraformProviderProductUserAgent, version.ProviderVersion)

terraformUA := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", terraformVersion, meta.SDKVersionString())
userAgent := fmt.Sprintf("%s %s/%s", terraformUA, TerraformProviderProductUserAgent, version.ProviderVersion)
accountID := data.Get("account_id").(int)

cfg := Config{
Expand Down
7 changes: 6 additions & 1 deletion newrelic/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ func TestAccNewRelicProvider_Region(t *testing.T) {
rName := acctest.RandString(5)

resource.ParallelTest(t, resource.TestCase{
Providers: testAccProviders,
Providers: testAccProviders,
CheckDestroy: func(*terraform.State) error { return nil },
Steps: []resource.TestStep{
// Test: Region "US"
{
Expand Down Expand Up @@ -131,6 +132,9 @@ func testAccPreCheck(t *testing.T) {

//testAccApplicationsCleanup(t)
testAccCreateApplication(t)

// We need to give the entity search engine time to index the app
time.Sleep(5 * time.Second)
}

func testAccCreateApplication(t *testing.T) {
Expand All @@ -148,6 +152,7 @@ func testAccCreateApplication(t *testing.T) {
}

app.RecordCustomEvent("terraform test", nil)

app.Shutdown(30 * time.Second)
}

Expand Down
272 changes: 272 additions & 0 deletions newrelic/resource_newrelic_entity_tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package newrelic

import (
"errors"
"fmt"
"log"
"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/newrelic/newrelic-client-go/pkg/entities"
nrErrors "github.com/newrelic/newrelic-client-go/pkg/errors"
)

var (
defaultTags = []string{
"account",
"accountId",
"language",
"trustedAccountId",
}
)

func resourceNewRelicEntityTags() *schema.Resource {
return &schema.Resource{
Create: resourceNewRelicEntityTagsCreate,
Read: resourceNewRelicEntityTagsRead,
Update: resourceNewRelicEntityTagsUpdate,
Delete: resourceNewRelicEntityTagsDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"guid": {
Type: schema.TypeString,
Required: true,
Description: "The guid of the entity to tag.",
},
"tag": {
Type: schema.TypeSet,
MinItems: 1,
Required: true,
Description: "A set of key-value pairs to represent a tag. For example: Team:TeamName",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {
Type: schema.TypeString,
Required: true,
Description: "The tag key.",
},
"values": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
MinItems: 1,
Required: true,
Description: "The tag values.",
},
},
},
},
},
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Second),
},
}
}

func resourceNewRelicEntityTagsCreate(d *schema.ResourceData, meta interface{}) error {
providerConfig := meta.(*ProviderConfig)

if !providerConfig.hasNerdGraphCredentials() {
return errors.New("err: NerdGraph support not present, but required for Create")
}

client := providerConfig.NewClient

guid := d.Get("guid").(string)
tags := expandEntityTags(d.Get("tag").(*schema.Set).List())

err := client.Entities.AddTags(guid, tags)
if err != nil {
return err
}

d.SetId(guid)

return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
currentTags, err := client.Entities.ListTags(guid)

if err != nil {
return resource.NonRetryableError(fmt.Errorf("error retrieving entity tags for guid %s: %s", d.Id(), err))
}

for _, t := range tags {
var tag *entities.Tag
if tag = getTag(currentTags, t.Key); tag == nil {
return resource.RetryableError(fmt.Errorf("expected entity tag %s to have been updated but was not found", t.Key))
}

if ok := tagValuesExist(tag, t.Values); !ok {
return resource.RetryableError(fmt.Errorf("expected entity tag values %s to have been updated for tag %s but were not found", t.Values, t.Key))
}
}

return resource.NonRetryableError(resourceNewRelicEntityTagsRead(d, meta))
})
}

func resourceNewRelicEntityTagsRead(d *schema.ResourceData, meta interface{}) error {
providerConfig := meta.(*ProviderConfig)

if !providerConfig.hasNerdGraphCredentials() {
return errors.New("err: NerdGraph support not present, but required for Read")
}

client := providerConfig.NewClient

log.Printf("[INFO] Reading New Relic entity tags for entity guid %s", d.Id())

tags, err := client.Entities.ListTags(d.Id())

if err != nil {
if _, ok := err.(*nrErrors.NotFound); ok {
d.SetId("")
return nil
}

return err
}

return flattenEntityTags(d, tags)
}

func resourceNewRelicEntityTagsUpdate(d *schema.ResourceData, meta interface{}) error {
providerConfig := meta.(*ProviderConfig)

if !providerConfig.hasNerdGraphCredentials() {
return errors.New("err: NerdGraph support not present, but required for Update")
}

client := providerConfig.NewClient

log.Printf("[INFO] Updating New Relic entity tags for entity guid %s", d.Id())

tags := expandEntityTags(d.Get("tag").(*schema.Set).List())

if err := client.Entities.ReplaceTags(d.Id(), tags); err != nil {
return err
}

return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
currentTags, err := client.Entities.ListTags(d.Id())

if err != nil {
return resource.NonRetryableError(fmt.Errorf("error retrieving entity tags for guid %s: %s", d.Id(), err))
}

for _, t := range tags {
var tag *entities.Tag
if tag = getTag(currentTags, t.Key); tag == nil {
return resource.RetryableError(fmt.Errorf("expected entity tag %s to have been updated but was not found", t.Key))
}

if ok := tagValuesExist(tag, t.Values); !ok {
return resource.RetryableError(fmt.Errorf("expected entity tag values %s to have been created for tag %s but were not found", t.Values, t.Key))
}
}

return resource.NonRetryableError(resourceNewRelicEntityTagsRead(d, meta))
})
}

func resourceNewRelicEntityTagsDelete(d *schema.ResourceData, meta interface{}) error {
providerConfig := meta.(*ProviderConfig)

if !providerConfig.hasNerdGraphCredentials() {
return errors.New("err: NerdGraph support not present, but required for Delete")
}

client := providerConfig.NewClient

log.Printf("[INFO] Deleting New Relic entity tags from entity guid %s", d.Id())

tags := expandEntityTags(d.Get("tag").(*schema.Set).List())
tagKeys := getTagKeys(tags)

if err := client.Entities.DeleteTags(d.Id(), tagKeys); err != nil {
return err
}

return nil
}

func expandEntityTags(tags []interface{}) []entities.Tag {
out := make([]entities.Tag, len(tags))

for i, rawCfg := range tags {
cfg := rawCfg.(map[string]interface{})
expanded := entities.Tag{
Key: cfg["key"].(string),
Values: expandEntityTagValues(cfg["values"].(*schema.Set).List()),
}

out[i] = expanded
}

return out
}

func expandEntityTagValues(values []interface{}) []string {
perms := make([]string, len(values))

for i, v := range values {
perms[i] = v.(string)
}

return perms
}

func flattenEntityTags(d *schema.ResourceData, tags []*entities.Tag) error {
out := []map[string]interface{}{}
for _, t := range tags {
if stringInSlice(defaultTags, t.Key) {
continue
}

m := make(map[string]interface{})
m["key"] = t.Key
m["values"] = t.Values

out = append(out, m)
}

if err := d.Set("guid", d.Id()); err != nil {
return err
}

if err := d.Set("tag", out); err != nil {
return err
}

return nil
}

func getTagKeys(tags []entities.Tag) []string {
tagKeys := []string{}

for _, t := range tags {
tagKeys = append(tagKeys, t.Key)
}
return tagKeys
}

func tagValuesExist(t *entities.Tag, values []string) bool {
for _, v := range values {
if !stringInSlice(t.Values, v) {
return false
}
}

return true
}

func getTag(tags []*entities.Tag, key string) *entities.Tag {
for _, t := range tags {
if t.Key == key {
return t
}
}

return nil
}
Loading