github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/resource_aws_kinesis_firehose_delivery_stream_test.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "strings" 8 "testing" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/service/firehose" 12 "github.com/hashicorp/terraform/helper/acctest" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/terraform" 15 ) 16 17 func TestAccAWSKinesisFirehoseDeliveryStream_s3basic(t *testing.T) { 18 var stream firehose.DeliveryStreamDescription 19 ri := acctest.RandInt() 20 config := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_s3basic, 21 ri, os.Getenv("AWS_ACCOUNT_ID"), ri, ri, ri) 22 23 resource.Test(t, resource.TestCase{ 24 PreCheck: testAccKinesisFirehosePreCheck(t), 25 Providers: testAccProviders, 26 CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, 27 Steps: []resource.TestStep{ 28 { 29 Config: config, 30 Check: resource.ComposeTestCheckFunc( 31 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 32 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, nil), 33 ), 34 }, 35 }, 36 }) 37 } 38 39 func TestAccAWSKinesisFirehoseDeliveryStream_s3WithCloudwatchLogging(t *testing.T) { 40 var stream firehose.DeliveryStreamDescription 41 ri := acctest.RandInt() 42 43 resource.Test(t, resource.TestCase{ 44 PreCheck: testAccKinesisFirehosePreCheck(t), 45 Providers: testAccProviders, 46 CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, 47 Steps: []resource.TestStep{ 48 { 49 Config: testAccKinesisFirehoseDeliveryStreamConfig_s3WithCloudwatchLogging(os.Getenv("AWS_ACCOUNT_ID"), ri), 50 Check: resource.ComposeTestCheckFunc( 51 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 52 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, nil), 53 ), 54 }, 55 }, 56 }) 57 } 58 59 func TestAccAWSKinesisFirehoseDeliveryStream_s3ConfigUpdates(t *testing.T) { 60 var stream firehose.DeliveryStreamDescription 61 62 ri := acctest.RandInt() 63 preConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_s3basic, 64 ri, os.Getenv("AWS_ACCOUNT_ID"), ri, ri, ri) 65 postConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_s3Updates, 66 ri, os.Getenv("AWS_ACCOUNT_ID"), ri, ri, ri) 67 68 updatedS3DestinationConfig := &firehose.S3DestinationDescription{ 69 BufferingHints: &firehose.BufferingHints{ 70 IntervalInSeconds: aws.Int64(400), 71 SizeInMBs: aws.Int64(10), 72 }, 73 } 74 75 resource.Test(t, resource.TestCase{ 76 PreCheck: testAccKinesisFirehosePreCheck(t), 77 Providers: testAccProviders, 78 CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, 79 Steps: []resource.TestStep{ 80 { 81 Config: preConfig, 82 Check: resource.ComposeTestCheckFunc( 83 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 84 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, nil), 85 ), 86 }, 87 88 { 89 Config: postConfig, 90 Check: resource.ComposeTestCheckFunc( 91 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 92 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, updatedS3DestinationConfig, nil, nil), 93 ), 94 }, 95 }, 96 }) 97 } 98 99 func TestAccAWSKinesisFirehoseDeliveryStream_RedshiftConfigUpdates(t *testing.T) { 100 var stream firehose.DeliveryStreamDescription 101 102 ri := acctest.RandInt() 103 preConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_RedshiftBasic, 104 ri, os.Getenv("AWS_ACCOUNT_ID"), ri, ri, ri, ri) 105 postConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_RedshiftUpdates, 106 ri, os.Getenv("AWS_ACCOUNT_ID"), ri, ri, ri, ri) 107 108 updatedRedshiftConfig := &firehose.RedshiftDestinationDescription{ 109 CopyCommand: &firehose.CopyCommand{ 110 CopyOptions: aws.String("GZIP"), 111 }, 112 } 113 114 resource.Test(t, resource.TestCase{ 115 PreCheck: testAccKinesisFirehosePreCheck(t), 116 Providers: testAccProviders, 117 CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, 118 Steps: []resource.TestStep{ 119 { 120 Config: preConfig, 121 Check: resource.ComposeTestCheckFunc( 122 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 123 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, nil), 124 ), 125 }, 126 127 { 128 Config: postConfig, 129 Check: resource.ComposeTestCheckFunc( 130 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream", &stream), 131 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, updatedRedshiftConfig, nil), 132 ), 133 }, 134 }, 135 }) 136 } 137 138 func TestAccAWSKinesisFirehoseDeliveryStream_ElasticsearchConfigUpdates(t *testing.T) { 139 var stream firehose.DeliveryStreamDescription 140 141 ri := acctest.RandInt() 142 awsAccountId := os.Getenv("AWS_ACCOUNT_ID") 143 preConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_ElasticsearchBasic, 144 ri, awsAccountId, ri, ri, ri, awsAccountId, awsAccountId, ri, ri) 145 postConfig := fmt.Sprintf(testAccKinesisFirehoseDeliveryStreamConfig_ElasticsearchUpdate, 146 ri, awsAccountId, ri, ri, ri, awsAccountId, awsAccountId, ri, ri) 147 148 updatedElasticSearchConfig := &firehose.ElasticsearchDestinationDescription{ 149 BufferingHints: &firehose.ElasticsearchBufferingHints{ 150 IntervalInSeconds: aws.Int64(500), 151 }, 152 } 153 154 resource.Test(t, resource.TestCase{ 155 PreCheck: testAccKinesisFirehosePreCheck(t), 156 Providers: testAccProviders, 157 CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, 158 Steps: []resource.TestStep{ 159 { 160 Config: preConfig, 161 Check: resource.ComposeTestCheckFunc( 162 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream_es", &stream), 163 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, nil), 164 ), 165 }, 166 { 167 Config: postConfig, 168 Check: resource.ComposeTestCheckFunc( 169 testAccCheckKinesisFirehoseDeliveryStreamExists("aws_kinesis_firehose_delivery_stream.test_stream_es", &stream), 170 testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(&stream, nil, nil, updatedElasticSearchConfig), 171 ), 172 }, 173 }, 174 }) 175 } 176 177 func testAccCheckKinesisFirehoseDeliveryStreamExists(n string, stream *firehose.DeliveryStreamDescription) resource.TestCheckFunc { 178 return func(s *terraform.State) error { 179 rs, ok := s.RootModule().Resources[n] 180 log.Printf("State: %#v", s.RootModule().Resources) 181 if !ok { 182 return fmt.Errorf("Not found: %s", n) 183 } 184 185 if rs.Primary.ID == "" { 186 return fmt.Errorf("No Kinesis Firehose ID is set") 187 } 188 189 conn := testAccProvider.Meta().(*AWSClient).firehoseconn 190 describeOpts := &firehose.DescribeDeliveryStreamInput{ 191 DeliveryStreamName: aws.String(rs.Primary.Attributes["name"]), 192 } 193 resp, err := conn.DescribeDeliveryStream(describeOpts) 194 if err != nil { 195 return err 196 } 197 198 *stream = *resp.DeliveryStreamDescription 199 200 return nil 201 } 202 } 203 204 func testAccCheckAWSKinesisFirehoseDeliveryStreamAttributes(stream *firehose.DeliveryStreamDescription, s3config interface{}, redshiftConfig interface{}, elasticsearchConfig interface{}) resource.TestCheckFunc { 205 return func(s *terraform.State) error { 206 if !strings.HasPrefix(*stream.DeliveryStreamName, "terraform-kinesis-firehose") { 207 return fmt.Errorf("Bad Stream name: %s", *stream.DeliveryStreamName) 208 } 209 for _, rs := range s.RootModule().Resources { 210 if rs.Type != "aws_kinesis_firehose_delivery_stream" { 211 continue 212 } 213 if *stream.DeliveryStreamARN != rs.Primary.Attributes["arn"] { 214 return fmt.Errorf("Bad Delivery Stream ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], *stream.DeliveryStreamARN) 215 } 216 217 if s3config != nil { 218 s := s3config.(*firehose.S3DestinationDescription) 219 // Range over the Stream Destinations, looking for the matching S3 220 // destination. For simplicity, our test only have a single S3 or 221 // Redshift destination, so at this time it's safe to match on the first 222 // one 223 var match bool 224 for _, d := range stream.Destinations { 225 if d.S3DestinationDescription != nil { 226 if *d.S3DestinationDescription.BufferingHints.SizeInMBs == *s.BufferingHints.SizeInMBs { 227 match = true 228 } 229 } 230 } 231 if !match { 232 return fmt.Errorf("Mismatch s3 buffer size, expected: %s, got: %s", s, stream.Destinations) 233 } 234 } 235 236 if redshiftConfig != nil { 237 r := redshiftConfig.(*firehose.RedshiftDestinationDescription) 238 // Range over the Stream Destinations, looking for the matching Redshift 239 // destination 240 var match bool 241 for _, d := range stream.Destinations { 242 if d.RedshiftDestinationDescription != nil { 243 if *d.RedshiftDestinationDescription.CopyCommand.CopyOptions == *r.CopyCommand.CopyOptions { 244 match = true 245 } 246 } 247 } 248 if !match { 249 return fmt.Errorf("Mismatch Redshift CopyOptions, expected: %s, got: %s", r, stream.Destinations) 250 } 251 } 252 253 if elasticsearchConfig != nil { 254 es := elasticsearchConfig.(*firehose.ElasticsearchDestinationDescription) 255 // Range over the Stream Destinations, looking for the matching Elasticsearch destination 256 var match bool 257 for _, d := range stream.Destinations { 258 if d.ElasticsearchDestinationDescription != nil { 259 match = true 260 } 261 } 262 if !match { 263 return fmt.Errorf("Mismatch Elasticsearch Buffering Interval, expected: %s, got: %s", es, stream.Destinations) 264 } 265 } 266 } 267 return nil 268 } 269 } 270 271 func testAccCheckKinesisFirehoseDeliveryStreamDestroy(s *terraform.State) error { 272 for _, rs := range s.RootModule().Resources { 273 if rs.Type != "aws_kinesis_firehose_delivery_stream" { 274 continue 275 } 276 conn := testAccProvider.Meta().(*AWSClient).firehoseconn 277 describeOpts := &firehose.DescribeDeliveryStreamInput{ 278 DeliveryStreamName: aws.String(rs.Primary.Attributes["name"]), 279 } 280 resp, err := conn.DescribeDeliveryStream(describeOpts) 281 if err == nil { 282 if resp.DeliveryStreamDescription != nil && *resp.DeliveryStreamDescription.DeliveryStreamStatus != "DELETING" { 283 return fmt.Errorf("Error: Delivery Stream still exists") 284 } 285 } 286 287 return nil 288 289 } 290 291 return nil 292 } 293 294 func testAccKinesisFirehosePreCheck(t *testing.T) func() { 295 return func() { 296 testAccPreCheck(t) 297 if os.Getenv("AWS_ACCOUNT_ID") == "" { 298 t.Fatal("AWS_ACCOUNT_ID must be set") 299 } 300 } 301 } 302 303 const testAccKinesisFirehoseDeliveryStreamBaseConfig = ` 304 resource "aws_iam_role" "firehose" { 305 name = "tf_acctest_firehose_delivery_role_%d" 306 assume_role_policy = <<EOF 307 { 308 "Version": "2012-10-17", 309 "Statement": [ 310 { 311 "Sid": "", 312 "Effect": "Allow", 313 "Principal": { 314 "Service": "firehose.amazonaws.com" 315 }, 316 "Action": "sts:AssumeRole", 317 "Condition": { 318 "StringEquals": { 319 "sts:ExternalId": "%s" 320 } 321 } 322 } 323 ] 324 } 325 EOF 326 } 327 328 resource "aws_s3_bucket" "bucket" { 329 bucket = "tf-test-bucket-%d" 330 acl = "private" 331 } 332 333 resource "aws_iam_role_policy" "firehose" { 334 name = "tf_acctest_firehose_delivery_policy_%d" 335 role = "${aws_iam_role.firehose.id}" 336 policy = <<EOF 337 { 338 "Version": "2012-10-17", 339 "Statement": [ 340 { 341 "Sid": "", 342 "Effect": "Allow", 343 "Action": [ 344 "s3:AbortMultipartUpload", 345 "s3:GetBucketLocation", 346 "s3:GetObject", 347 "s3:ListBucket", 348 "s3:ListBucketMultipartUploads", 349 "s3:PutObject" 350 ], 351 "Resource": [ 352 "arn:aws:s3:::${aws_s3_bucket.bucket.id}", 353 "arn:aws:s3:::${aws_s3_bucket.bucket.id}/*" 354 ] 355 } 356 ] 357 } 358 EOF 359 } 360 361 ` 362 363 func testAccKinesisFirehoseDeliveryStreamConfig_s3WithCloudwatchLogging(accountId string, rInt int) string { 364 return fmt.Sprintf(` 365 resource "aws_iam_role" "firehose" { 366 name = "tf_acctest_firehose_delivery_role_%d" 367 assume_role_policy = <<EOF 368 { 369 "Version": "2012-10-17", 370 "Statement": [ 371 { 372 "Sid": "", 373 "Effect": "Allow", 374 "Principal": { 375 "Service": "firehose.amazonaws.com" 376 }, 377 "Action": "sts:AssumeRole", 378 "Condition": { 379 "StringEquals": { 380 "sts:ExternalId": "%s" 381 } 382 } 383 } 384 ] 385 } 386 EOF 387 } 388 389 resource "aws_iam_role_policy" "firehose" { 390 name = "tf_acctest_firehose_delivery_policy_%d" 391 role = "${aws_iam_role.firehose.id}" 392 policy = <<EOF 393 { 394 "Version": "2012-10-17", 395 "Statement": [ 396 { 397 "Sid": "", 398 "Effect": "Allow", 399 "Action": [ 400 "s3:AbortMultipartUpload", 401 "s3:GetBucketLocation", 402 "s3:GetObject", 403 "s3:ListBucket", 404 "s3:ListBucketMultipartUploads", 405 "s3:PutObject" 406 ], 407 "Resource": [ 408 "arn:aws:s3:::${aws_s3_bucket.bucket.id}", 409 "arn:aws:s3:::${aws_s3_bucket.bucket.id}/*" 410 ] 411 }, 412 { 413 "Effect": "Allow", 414 "Action": [ 415 "logs:putLogEvents" 416 ], 417 "Resource": [ 418 "arn:aws:logs::log-group:/aws/kinesisfirehose/*" 419 ] 420 } 421 ] 422 } 423 EOF 424 } 425 426 resource "aws_s3_bucket" "bucket" { 427 bucket = "tf-test-bucket-%d" 428 acl = "private" 429 } 430 431 resource "aws_cloudwatch_log_group" "test" { 432 name = "example-%d" 433 } 434 435 resource "aws_cloudwatch_log_stream" "test" { 436 name = "sample-log-stream-test-%d" 437 log_group_name = "${aws_cloudwatch_log_group.test.name}" 438 } 439 440 resource "aws_kinesis_firehose_delivery_stream" "test_stream" { 441 depends_on = ["aws_iam_role_policy.firehose"] 442 name = "terraform-kinesis-firehose-cloudwatch-%d" 443 destination = "s3" 444 s3_configuration { 445 role_arn = "${aws_iam_role.firehose.arn}" 446 bucket_arn = "${aws_s3_bucket.bucket.arn}" 447 cloudwatch_logging_options { 448 enabled = true 449 log_group_name = "${aws_cloudwatch_log_group.test.name}" 450 log_stream_name = "${aws_cloudwatch_log_stream.test.name}" 451 } 452 } 453 } 454 `, rInt, accountId, rInt, rInt, rInt, rInt, rInt) 455 } 456 457 var testAccKinesisFirehoseDeliveryStreamConfig_s3basic = testAccKinesisFirehoseDeliveryStreamBaseConfig + ` 458 resource "aws_kinesis_firehose_delivery_stream" "test_stream" { 459 depends_on = ["aws_iam_role_policy.firehose"] 460 name = "terraform-kinesis-firehose-basictest-%d" 461 destination = "s3" 462 s3_configuration { 463 role_arn = "${aws_iam_role.firehose.arn}" 464 bucket_arn = "${aws_s3_bucket.bucket.arn}" 465 } 466 }` 467 468 var testAccKinesisFirehoseDeliveryStreamConfig_s3Updates = testAccKinesisFirehoseDeliveryStreamBaseConfig + ` 469 resource "aws_kinesis_firehose_delivery_stream" "test_stream" { 470 depends_on = ["aws_iam_role_policy.firehose"] 471 name = "terraform-kinesis-firehose-s3test-%d" 472 destination = "s3" 473 s3_configuration { 474 role_arn = "${aws_iam_role.firehose.arn}" 475 bucket_arn = "${aws_s3_bucket.bucket.arn}" 476 buffer_size = 10 477 buffer_interval = 400 478 compression_format = "GZIP" 479 } 480 }` 481 482 var testAccKinesisFirehoseDeliveryStreamBaseRedshiftConfig = testAccKinesisFirehoseDeliveryStreamBaseConfig + ` 483 resource "aws_redshift_cluster" "test_cluster" { 484 cluster_identifier = "tf-redshift-cluster-%d" 485 database_name = "test" 486 master_username = "testuser" 487 master_password = "T3stPass" 488 node_type = "dc1.large" 489 cluster_type = "single-node" 490 skip_final_snapshot = true 491 }` 492 493 var testAccKinesisFirehoseDeliveryStreamConfig_RedshiftBasic = testAccKinesisFirehoseDeliveryStreamBaseRedshiftConfig + ` 494 resource "aws_kinesis_firehose_delivery_stream" "test_stream" { 495 depends_on = ["aws_iam_role_policy.firehose", "aws_redshift_cluster.test_cluster"] 496 name = "terraform-kinesis-firehose-basicredshifttest-%d" 497 destination = "redshift" 498 s3_configuration { 499 role_arn = "${aws_iam_role.firehose.arn}" 500 bucket_arn = "${aws_s3_bucket.bucket.arn}" 501 } 502 redshift_configuration { 503 role_arn = "${aws_iam_role.firehose.arn}" 504 cluster_jdbcurl = "jdbc:redshift://${aws_redshift_cluster.test_cluster.endpoint}/${aws_redshift_cluster.test_cluster.database_name}" 505 username = "testuser" 506 password = "T3stPass" 507 data_table_name = "test-table" 508 } 509 }` 510 511 var testAccKinesisFirehoseDeliveryStreamConfig_RedshiftUpdates = testAccKinesisFirehoseDeliveryStreamBaseRedshiftConfig + ` 512 resource "aws_kinesis_firehose_delivery_stream" "test_stream" { 513 depends_on = ["aws_iam_role_policy.firehose", "aws_redshift_cluster.test_cluster"] 514 name = "terraform-kinesis-firehose-basicredshifttest-%d" 515 destination = "redshift" 516 s3_configuration { 517 role_arn = "${aws_iam_role.firehose.arn}" 518 bucket_arn = "${aws_s3_bucket.bucket.arn}" 519 buffer_size = 10 520 buffer_interval = 400 521 compression_format = "GZIP" 522 } 523 redshift_configuration { 524 role_arn = "${aws_iam_role.firehose.arn}" 525 cluster_jdbcurl = "jdbc:redshift://${aws_redshift_cluster.test_cluster.endpoint}/${aws_redshift_cluster.test_cluster.database_name}" 526 username = "testuser" 527 password = "T3stPass" 528 data_table_name = "test-table" 529 copy_options = "GZIP" 530 data_table_columns = "test-col" 531 } 532 }` 533 534 var testAccKinesisFirehoseDeliveryStreamBaseElasticsearchConfig = testAccKinesisFirehoseDeliveryStreamBaseConfig + ` 535 resource "aws_elasticsearch_domain" "test_cluster" { 536 domain_name = "es-test-%d" 537 cluster_config { 538 instance_type = "r3.large.elasticsearch" 539 } 540 541 access_policies = <<CONFIG 542 { 543 "Version": "2012-10-17", 544 "Statement": [ 545 { 546 "Effect": "Allow", 547 "Principal": { 548 "AWS": "arn:aws:iam::%s:root" 549 }, 550 "Action": "es:*", 551 "Resource": "arn:aws:es:us-east-1:%s:domain/es-test-%d/*" 552 } 553 ] 554 } 555 CONFIG 556 }` 557 558 var testAccKinesisFirehoseDeliveryStreamConfig_ElasticsearchBasic = testAccKinesisFirehoseDeliveryStreamBaseElasticsearchConfig + ` 559 resource "aws_kinesis_firehose_delivery_stream" "test_stream_es" { 560 depends_on = ["aws_iam_role_policy.firehose", "aws_elasticsearch_domain.test_cluster"] 561 name = "terraform-kinesis-firehose-es-%d" 562 destination = "elasticsearch" 563 s3_configuration { 564 role_arn = "${aws_iam_role.firehose.arn}" 565 bucket_arn = "${aws_s3_bucket.bucket.arn}" 566 } 567 elasticsearch_configuration { 568 domain_arn = "${aws_elasticsearch_domain.test_cluster.arn}" 569 role_arn = "${aws_iam_role.firehose.arn}" 570 index_name = "test" 571 type_name = "test" 572 } 573 }` 574 575 var testAccKinesisFirehoseDeliveryStreamConfig_ElasticsearchUpdate = testAccKinesisFirehoseDeliveryStreamBaseElasticsearchConfig + ` 576 resource "aws_kinesis_firehose_delivery_stream" "test_stream_es" { 577 depends_on = ["aws_iam_role_policy.firehose", "aws_elasticsearch_domain.test_cluster"] 578 name = "terraform-kinesis-firehose-es-%d" 579 destination = "elasticsearch" 580 s3_configuration { 581 role_arn = "${aws_iam_role.firehose.arn}" 582 bucket_arn = "${aws_s3_bucket.bucket.arn}" 583 } 584 elasticsearch_configuration { 585 domain_arn = "${aws_elasticsearch_domain.test_cluster.arn}" 586 role_arn = "${aws_iam_role.firehose.arn}" 587 index_name = "test" 588 type_name = "test" 589 buffering_interval = 500 590 } 591 }`