From 01dda66031bb266f2e48361bbc181c1a4b2cb74a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 13 Feb 2018 15:14:03 -0500 Subject: [PATCH 1/2] resource/aws_sns_topic: Fix exit after updating first attribute --- aws/resource_aws_sns_topic.go | 134 +++++++++++++---------------- aws/resource_aws_sns_topic_test.go | 13 ++- 2 files changed, 67 insertions(+), 80 deletions(-) diff --git a/aws/resource_aws_sns_topic.go b/aws/resource_aws_sns_topic.go index 2f8a55f4c841..3fba83d9a0d2 100644 --- a/aws/resource_aws_sns_topic.go +++ b/aws/resource_aws_sns_topic.go @@ -6,9 +6,7 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/sns" - "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/structure" @@ -16,22 +14,23 @@ import ( // Mutable attributes var SNSAttributeMap = map[string]string{ - "arn": "TopicArn", - "display_name": "DisplayName", - "policy": "Policy", - "delivery_policy": "DeliveryPolicy", + "application_failure_feedback_role_arn": "ApplicationFailureFeedbackRoleArn", "application_success_feedback_role_arn": "ApplicationSuccessFeedbackRoleArn", "application_success_feedback_sample_rate": "ApplicationSuccessFeedbackSampleRate", - "application_failure_feedback_role_arn": "ApplicationFailureFeedbackRoleArn", - "http_success_feedback_role_arn": "HTTPSuccessFeedbackRoleArn", - "http_success_feedback_sample_rate": "HTTPSuccessFeedbackSampleRate", - "http_failure_feedback_role_arn": "HTTPFailureFeedbackRoleArn", - "lambda_success_feedback_role_arn": "LambdaSuccessFeedbackRoleArn", - "lambda_success_feedback_sample_rate": "LambdaSuccessFeedbackSampleRate", - "lambda_failure_feedback_role_arn": "LambdaFailureFeedbackRoleArn", - "sqs_success_feedback_role_arn": "SQSSuccessFeedbackRoleArn", - "sqs_success_feedback_sample_rate": "SQSSuccessFeedbackSampleRate", - "sqs_failure_feedback_role_arn": "SQSFailureFeedbackRoleArn"} + "arn": "TopicArn", + "delivery_policy": "DeliveryPolicy", + "display_name": "DisplayName", + "http_failure_feedback_role_arn": "HTTPFailureFeedbackRoleArn", + "http_success_feedback_role_arn": "HTTPSuccessFeedbackRoleArn", + "http_success_feedback_sample_rate": "HTTPSuccessFeedbackSampleRate", + "lambda_failure_feedback_role_arn": "LambdaFailureFeedbackRoleArn", + "lambda_success_feedback_role_arn": "LambdaSuccessFeedbackRoleArn", + "lambda_success_feedback_sample_rate": "LambdaSuccessFeedbackSampleRate", + "policy": "Policy", + "sqs_failure_feedback_role_arn": "SQSFailureFeedbackRoleArn", + "sqs_success_feedback_role_arn": "SQSSuccessFeedbackRoleArn", + "sqs_success_feedback_sample_rate": "SQSSuccessFeedbackSampleRate", +} func resourceAwsSnsTopic() *schema.Resource { return &schema.Resource{ @@ -44,24 +43,23 @@ func resourceAwsSnsTopic() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ + "name": { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, ConflictsWith: []string{"name_prefix"}, }, - "name_prefix": &schema.Schema{ + "name_prefix": { Type: schema.TypeString, Optional: true, ForceNew: true, }, - "display_name": &schema.Schema{ + "display_name": { Type: schema.TypeString, Optional: true, - ForceNew: false, }, - "policy": &schema.Schema{ + "policy": { Type: schema.TypeString, Optional: true, Computed: true, @@ -72,7 +70,7 @@ func resourceAwsSnsTopic() *schema.Resource { return json }, }, - "delivery_policy": &schema.Schema{ + "delivery_policy": { Type: schema.TypeString, Optional: true, ForceNew: false, @@ -135,7 +133,7 @@ func resourceAwsSnsTopic() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "arn": &schema.Schema{ + "arn": { Type: schema.TypeString, Computed: true, }, @@ -168,37 +166,18 @@ func resourceAwsSnsTopicCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(*output.TopicArn) - // Write the ARN to the 'arn' field for export - d.Set("arn", *output.TopicArn) - return resourceAwsSnsTopicUpdate(d, meta) } func resourceAwsSnsTopicUpdate(d *schema.ResourceData, meta interface{}) error { - r := *resourceAwsSnsTopic() + conn := meta.(*AWSClient).snsconn - for k, _ := range r.Schema { - if attrKey, ok := SNSAttributeMap[k]; ok { - if d.HasChange(k) { - log.Printf("[DEBUG] Updating %s", attrKey) - _, n := d.GetChange(k) - // Ignore an empty policy - if !(k == "policy" && n == "") { - // Make API call to update attributes - req := sns.SetTopicAttributesInput{ - TopicArn: aws.String(d.Id()), - AttributeName: aws.String(attrKey), - AttributeValue: aws.String(fmt.Sprintf("%v", n)), - } - conn := meta.(*AWSClient).snsconn - // Retry the update in the event of an eventually consistent style of - // error, where say an IAM resource is successfully created but not - // actually available. See https://github.com/hashicorp/terraform/issues/3660 - _, err := retryOnAwsCode("InvalidParameter", func() (interface{}, error) { - return conn.SetTopicAttributes(&req) - }) - return err - } + for terraformAttrName, snsAttrName := range SNSAttributeMap { + if d.HasChange(terraformAttrName) { + _, terraformAttrValue := d.GetChange(terraformAttrName) + err := updateAwsSnsTopicAttribute(d.Id(), snsAttrName, terraformAttrValue, conn) + if err != nil { + return err } } } @@ -209,11 +188,12 @@ func resourceAwsSnsTopicUpdate(d *schema.ResourceData, meta interface{}) error { func resourceAwsSnsTopicRead(d *schema.ResourceData, meta interface{}) error { snsconn := meta.(*AWSClient).snsconn + log.Printf("[DEBUG] Reading SNS Topic Attributes for %s", d.Id()) attributeOutput, err := snsconn.GetTopicAttributes(&sns.GetTopicAttributesInput{ TopicArn: aws.String(d.Id()), }) if err != nil { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFound" { + if isAWSErr(err, sns.ErrCodeNotFoundException, "") { log.Printf("[WARN] SNS Topic (%s) not found, error code (404)", d.Id()) d.SetId("") return nil @@ -224,28 +204,12 @@ func resourceAwsSnsTopicRead(d *schema.ResourceData, meta interface{}) error { if attributeOutput.Attributes != nil && len(attributeOutput.Attributes) > 0 { attrmap := attributeOutput.Attributes - resource := *resourceAwsSnsTopic() - // iKey = internal struct key, oKey = AWS Attribute Map key - for iKey, oKey := range SNSAttributeMap { - log.Printf("[DEBUG] Reading %s => %s", iKey, oKey) - - if attrmap[oKey] != nil { - // Some of the fetched attributes are stateful properties such as - // the number of subscriptions, the owner, etc. skip those - if resource.Schema[iKey] != nil { - var value string - if iKey == "policy" { - value, err = structure.NormalizeJsonString(*attrmap[oKey]) - if err != nil { - return errwrap.Wrapf("policy contains an invalid JSON: {{err}}", err) - } - } else { - value = *attrmap[oKey] - } - log.Printf("[DEBUG] Reading %s => %s -> %s", iKey, oKey, value) - d.Set(iKey, value) - } - } + for terraformAttrName, snsAttrName := range SNSAttributeMap { + d.Set(terraformAttrName, attrmap[snsAttrName]) + } + } else { + for terraformAttrName, _ := range SNSAttributeMap { + d.Set(terraformAttrName, "") } } @@ -275,3 +239,29 @@ func resourceAwsSnsTopicDelete(d *schema.ResourceData, meta interface{}) error { } return nil } + +func updateAwsSnsTopicAttribute(topicArn, name string, value interface{}, conn *sns.SNS) error { + // Ignore an empty policy + if name == "Policy" && value == "" { + return nil + } + log.Printf("[DEBUG] Updating SNS Topic Attribute: %s", name) + + // Make API call to update attributes + req := sns.SetTopicAttributesInput{ + TopicArn: aws.String(topicArn), + AttributeName: aws.String(name), + AttributeValue: aws.String(fmt.Sprintf("%v", value)), + } + + // Retry the update in the event of an eventually consistent style of + // error, where say an IAM resource is successfully created but not + // actually available. See https://github.com/hashicorp/terraform/issues/3660 + _, err := retryOnAwsCode(sns.ErrCodeInvalidParameterException, func() (interface{}, error) { + return conn.SetTopicAttributes(&req) + }) + if err != nil { + return err + } + return nil +} diff --git a/aws/resource_aws_sns_topic_test.go b/aws/resource_aws_sns_topic_test.go index c8fe55461913..2ae0434e8452 100644 --- a/aws/resource_aws_sns_topic_test.go +++ b/aws/resource_aws_sns_topic_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/sns" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" @@ -278,15 +277,13 @@ func testAccCheckAWSSNSTopicDestroy(s *terraform.State) error { TopicArn: aws.String(rs.Primary.ID), } _, err := conn.GetTopicAttributes(params) - if err == nil { - return fmt.Errorf("Topic exists when it should be destroyed!") - } - - // Verify the error is an API error, not something else - _, ok := err.(awserr.Error) - if !ok { + if err != nil { + if isAWSErr(err, sns.ErrCodeNotFoundException, "") { + return nil + } return err } + return fmt.Errorf("Topic exists when it should be destroyed!") } return nil From ce07621205902e08408d36be84e2f78b95e13abe Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Feb 2018 18:43:27 -0500 Subject: [PATCH 2/2] test/resource/aws_sns_topic: Add testAccCheckAWSSNSTopicAttributes helper and verify TestAccAWSSNSTopic_deliveryStatus fix --- aws/resource_aws_sns_topic_policy_test.go | 4 +- ...esource_aws_sns_topic_subscription_test.go | 12 +++- aws/resource_aws_sns_topic_test.go | 66 ++++++++++++++++--- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/aws/resource_aws_sns_topic_policy_test.go b/aws/resource_aws_sns_topic_policy_test.go index 4aae9645b41c..3f5f4b3727fc 100644 --- a/aws/resource_aws_sns_topic_policy_test.go +++ b/aws/resource_aws_sns_topic_policy_test.go @@ -8,6 +8,8 @@ import ( ) func TestAccAWSSNSTopicPolicy_basic(t *testing.T) { + attributes := make(map[string]string) + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -16,7 +18,7 @@ func TestAccAWSSNSTopicPolicy_basic(t *testing.T) { { Config: testAccAWSSNSTopicConfig_withPolicy, Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test", attributes), resource.TestMatchResourceAttr("aws_sns_topic_policy.custom", "policy", regexp.MustCompile("^{\"Version\":\"2012-10-17\".+")), ), diff --git a/aws/resource_aws_sns_topic_subscription_test.go b/aws/resource_aws_sns_topic_subscription_test.go index df9170614dfb..6988057dd842 100644 --- a/aws/resource_aws_sns_topic_subscription_test.go +++ b/aws/resource_aws_sns_topic_subscription_test.go @@ -14,6 +14,8 @@ import ( ) func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { + attributes := make(map[string]string) + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ @@ -24,7 +26,7 @@ func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { { Config: testAccAWSSNSTopicSubscriptionConfig(ri), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), testAccCheckAWSSNSTopicSubscriptionExists("aws_sns_topic_subscription.test_subscription"), ), }, @@ -54,6 +56,8 @@ func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { }) } func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { + attributes := make(map[string]string) + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ @@ -64,7 +68,7 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { { Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(ri), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), testAccCheckAWSSNSTopicSubscriptionExists("aws_sns_topic_subscription.test_subscription"), ), }, @@ -73,6 +77,8 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { } func TestAccAWSSNSTopicSubscription_autoConfirmingSecuredEndpoint(t *testing.T) { + attributes := make(map[string]string) + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ @@ -83,7 +89,7 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingSecuredEndpoint(t *testing.T) { Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingSecuredEndpoint(ri, "john", "doe"), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), testAccCheckAWSSNSTopicSubscriptionExists("aws_sns_topic_subscription.test_subscription"), ), }, diff --git a/aws/resource_aws_sns_topic_test.go b/aws/resource_aws_sns_topic_test.go index 2ae0434e8452..20b716f80b72 100644 --- a/aws/resource_aws_sns_topic_test.go +++ b/aws/resource_aws_sns_topic_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sns" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -14,6 +15,8 @@ import ( ) func TestAccAWSSNSTopic_basic(t *testing.T) { + attributes := make(map[string]string) + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, IDRefreshName: "aws_sns_topic.test_topic", @@ -23,7 +26,7 @@ func TestAccAWSSNSTopic_basic(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_withGeneratedName, Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), ), }, }, @@ -31,6 +34,8 @@ func TestAccAWSSNSTopic_basic(t *testing.T) { } func TestAccAWSSNSTopic_name(t *testing.T) { + attributes := make(map[string]string) + rName := acctest.RandString(10) resource.Test(t, resource.TestCase{ @@ -42,7 +47,7 @@ func TestAccAWSSNSTopic_name(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_withName(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), ), }, }, @@ -50,6 +55,8 @@ func TestAccAWSSNSTopic_name(t *testing.T) { } func TestAccAWSSNSTopic_namePrefix(t *testing.T) { + attributes := make(map[string]string) + startsWithPrefix := regexp.MustCompile("^terraform-test-topic-") resource.Test(t, resource.TestCase{ @@ -61,7 +68,7 @@ func TestAccAWSSNSTopic_namePrefix(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_withNamePrefix(), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), resource.TestMatchResourceAttr("aws_sns_topic.test_topic", "name", startsWithPrefix), ), }, @@ -70,6 +77,8 @@ func TestAccAWSSNSTopic_namePrefix(t *testing.T) { } func TestAccAWSSNSTopic_policy(t *testing.T) { + attributes := make(map[string]string) + rName := acctest.RandString(10) expectedPolicy := `{"Statement":[{"Sid":"Stmt1445931846145","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sns:Publish","Resource":"arn:aws:sns:us-west-2::example"}],"Version":"2012-10-17","Id":"Policy1445931846145"}` resource.Test(t, resource.TestCase{ @@ -81,7 +90,7 @@ func TestAccAWSSNSTopic_policy(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicWithPolicy(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), testAccCheckAWSNSTopicHasPolicy("aws_sns_topic.test_topic", expectedPolicy), ), }, @@ -90,6 +99,8 @@ func TestAccAWSSNSTopic_policy(t *testing.T) { } func TestAccAWSSNSTopic_withIAMRole(t *testing.T) { + attributes := make(map[string]string) + rName := acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -100,7 +111,7 @@ func TestAccAWSSNSTopic_withIAMRole(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_withIAMRole(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), ), }, }, @@ -124,6 +135,8 @@ func TestAccAWSSNSTopic_withFakeIAMRole(t *testing.T) { } func TestAccAWSSNSTopic_withDeliveryPolicy(t *testing.T) { + attributes := make(map[string]string) + rName := acctest.RandString(10) expectedPolicy := `{"http":{"defaultHealthyRetryPolicy": {"minDelayTarget": 20,"maxDelayTarget": 20,"numMaxDelayRetries": 0,"numRetries": 3,"numNoDelayRetries": 0,"numMinDelayRetries": 0,"backoffFunction": "linear"},"disableSubscriptionOverrides": false}}` resource.Test(t, resource.TestCase{ @@ -135,7 +148,7 @@ func TestAccAWSSNSTopic_withDeliveryPolicy(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_withDeliveryPolicy(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), testAccCheckAWSNSTopicHasDeliveryPolicy("aws_sns_topic.test_topic", expectedPolicy), ), }, @@ -144,8 +157,25 @@ func TestAccAWSSNSTopic_withDeliveryPolicy(t *testing.T) { } func TestAccAWSSNSTopic_deliveryStatus(t *testing.T) { + attributes := make(map[string]string) + rName := acctest.RandString(10) arnRegex := regexp.MustCompile("^arn:aws:iam::[0-9]{12}:role/sns-delivery-status-role-") + expectedAttributes := map[string]*regexp.Regexp{ + "ApplicationFailureFeedbackRoleArn": arnRegex, + "ApplicationSuccessFeedbackRoleArn": arnRegex, + "ApplicationSuccessFeedbackSampleRate": regexp.MustCompile(`^100$`), + "HTTPFailureFeedbackRoleArn": arnRegex, + "HTTPSuccessFeedbackRoleArn": arnRegex, + "HTTPSuccessFeedbackSampleRate": regexp.MustCompile(`^80$`), + "LambdaFailureFeedbackRoleArn": arnRegex, + "LambdaSuccessFeedbackRoleArn": arnRegex, + "LambdaSuccessFeedbackSampleRate": regexp.MustCompile(`^90$`), + "SQSFailureFeedbackRoleArn": arnRegex, + "SQSSuccessFeedbackRoleArn": arnRegex, + "SQSSuccessFeedbackSampleRate": regexp.MustCompile(`^70$`), + } + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, IDRefreshName: "aws_sns_topic.test_topic", @@ -155,7 +185,8 @@ func TestAccAWSSNSTopic_deliveryStatus(t *testing.T) { resource.TestStep{ Config: testAccAWSSNSTopicConfig_deliveryStatus(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic", attributes), + testAccCheckAWSSNSTopicAttributes(attributes, expectedAttributes), resource.TestMatchResourceAttr("aws_sns_topic.test_topic", "application_success_feedback_role_arn", arnRegex), resource.TestCheckResourceAttr("aws_sns_topic.test_topic", "application_success_feedback_sample_rate", "100"), resource.TestMatchResourceAttr("aws_sns_topic.test_topic", "application_failure_feedback_role_arn", arnRegex), @@ -289,7 +320,20 @@ func testAccCheckAWSSNSTopicDestroy(s *terraform.State) error { return nil } -func testAccCheckAWSSNSTopicExists(n string) resource.TestCheckFunc { +func testAccCheckAWSSNSTopicAttributes(attributes map[string]string, expectedAttributes map[string]*regexp.Regexp) resource.TestCheckFunc { + return func(s *terraform.State) error { + var errors error + for k, expectedR := range expectedAttributes { + if v, ok := attributes[k]; !ok || !expectedR.MatchString(v) { + err := fmt.Errorf("expected SNS topic attribute %q to match %q, received: %q", k, expectedR.String(), v) + errors = multierror.Append(errors, err) + } + } + return errors + } +} + +func testAccCheckAWSSNSTopicExists(n string, attributes map[string]string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -305,12 +349,16 @@ func testAccCheckAWSSNSTopicExists(n string) resource.TestCheckFunc { params := &sns.GetTopicAttributesInput{ TopicArn: aws.String(rs.Primary.ID), } - _, err := conn.GetTopicAttributes(params) + out, err := conn.GetTopicAttributes(params) if err != nil { return err } + for k, v := range out.Attributes { + attributes[k] = *v + } + return nil } }