github.com/danrjohnson/terraform@v0.7.0-rc2.0.20160627135212-d0fc1fa086ff/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/hashicorp/terraform/helper/hashcode" 12 "github.com/hashicorp/terraform/helper/resource" 13 "github.com/hashicorp/terraform/helper/schema" 14 15 "github.com/aws/aws-sdk-go/aws" 16 "github.com/aws/aws-sdk-go/service/ec2" 17 "github.com/aws/aws-sdk-go/service/elasticbeanstalk" 18 ) 19 20 func resourceAwsElasticBeanstalkOptionSetting() *schema.Resource { 21 return &schema.Resource{ 22 Schema: map[string]*schema.Schema{ 23 "namespace": &schema.Schema{ 24 Type: schema.TypeString, 25 Required: true, 26 }, 27 "name": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 }, 31 "value": &schema.Schema{ 32 Type: schema.TypeString, 33 Required: true, 34 }, 35 }, 36 } 37 } 38 39 func resourceAwsElasticBeanstalkEnvironment() *schema.Resource { 40 return &schema.Resource{ 41 Create: resourceAwsElasticBeanstalkEnvironmentCreate, 42 Read: resourceAwsElasticBeanstalkEnvironmentRead, 43 Update: resourceAwsElasticBeanstalkEnvironmentUpdate, 44 Delete: resourceAwsElasticBeanstalkEnvironmentDelete, 45 46 SchemaVersion: 1, 47 MigrateState: resourceAwsElasticBeanstalkEnvironmentMigrateState, 48 49 Schema: map[string]*schema.Schema{ 50 "name": &schema.Schema{ 51 Type: schema.TypeString, 52 Required: true, 53 ForceNew: true, 54 }, 55 "application": &schema.Schema{ 56 Type: schema.TypeString, 57 Required: true, 58 }, 59 "description": &schema.Schema{ 60 Type: schema.TypeString, 61 Optional: true, 62 }, 63 "cname": &schema.Schema{ 64 Type: schema.TypeString, 65 Computed: true, 66 }, 67 "cname_prefix": &schema.Schema{ 68 Type: schema.TypeString, 69 Computed: true, 70 Optional: true, 71 ForceNew: true, 72 }, 73 "tier": &schema.Schema{ 74 Type: schema.TypeString, 75 Optional: true, 76 Default: "WebServer", 77 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 78 value := v.(string) 79 switch value { 80 case 81 "Worker", 82 "WebServer": 83 return 84 } 85 errors = append(errors, fmt.Errorf("%s is not a valid tier. Valid options are WebServer or Worker", value)) 86 return 87 }, 88 ForceNew: true, 89 }, 90 "setting": &schema.Schema{ 91 Type: schema.TypeSet, 92 Optional: true, 93 Computed: true, 94 Elem: resourceAwsElasticBeanstalkOptionSetting(), 95 Set: optionSettingValueHash, 96 }, 97 "all_settings": &schema.Schema{ 98 Type: schema.TypeSet, 99 Computed: true, 100 Elem: resourceAwsElasticBeanstalkOptionSetting(), 101 Set: optionSettingValueHash, 102 }, 103 "solution_stack_name": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 }, 107 "template_name": &schema.Schema{ 108 Type: schema.TypeString, 109 Optional: true, 110 ConflictsWith: []string{"solution_stack_name"}, 111 }, 112 "wait_for_ready_timeout": &schema.Schema{ 113 Type: schema.TypeString, 114 Optional: true, 115 Default: "10m", 116 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 117 value := v.(string) 118 duration, err := time.ParseDuration(value) 119 if err != nil { 120 errors = append(errors, fmt.Errorf( 121 "%q cannot be parsed as a duration: %s", k, err)) 122 } 123 if duration < 0 { 124 errors = append(errors, fmt.Errorf( 125 "%q must be greater than zero", k)) 126 } 127 return 128 }, 129 }, 130 "autoscaling_groups": &schema.Schema{ 131 Type: schema.TypeList, 132 Computed: true, 133 Elem: &schema.Schema{Type: schema.TypeString}, 134 }, 135 "instances": &schema.Schema{ 136 Type: schema.TypeList, 137 Computed: true, 138 Elem: &schema.Schema{Type: schema.TypeString}, 139 }, 140 "launch_configurations": &schema.Schema{ 141 Type: schema.TypeList, 142 Computed: true, 143 Elem: &schema.Schema{Type: schema.TypeString}, 144 }, 145 "load_balancers": &schema.Schema{ 146 Type: schema.TypeList, 147 Computed: true, 148 Elem: &schema.Schema{Type: schema.TypeString}, 149 }, 150 "queues": &schema.Schema{ 151 Type: schema.TypeList, 152 Computed: true, 153 Elem: &schema.Schema{Type: schema.TypeString}, 154 }, 155 "triggers": &schema.Schema{ 156 Type: schema.TypeList, 157 Computed: true, 158 Elem: &schema.Schema{Type: schema.TypeString}, 159 }, 160 161 "tags": tagsSchema(), 162 }, 163 } 164 } 165 166 func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { 167 conn := meta.(*AWSClient).elasticbeanstalkconn 168 169 // Get values from config 170 name := d.Get("name").(string) 171 cnamePrefix := d.Get("cname_prefix").(string) 172 tier := d.Get("tier").(string) 173 app := d.Get("application").(string) 174 desc := d.Get("description").(string) 175 settings := d.Get("setting").(*schema.Set) 176 solutionStack := d.Get("solution_stack_name").(string) 177 templateName := d.Get("template_name").(string) 178 waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string)) 179 if err != nil { 180 return err 181 } 182 183 // TODO set tags 184 // Note: at time of writing, you cannot view or edit Tags after creation 185 // d.Set("tags", tagsToMap(instance.Tags)) 186 createOpts := elasticbeanstalk.CreateEnvironmentInput{ 187 EnvironmentName: aws.String(name), 188 ApplicationName: aws.String(app), 189 OptionSettings: extractOptionSettings(settings), 190 Tags: tagsFromMapBeanstalk(d.Get("tags").(map[string]interface{})), 191 } 192 193 if desc != "" { 194 createOpts.Description = aws.String(desc) 195 } 196 197 if cnamePrefix != "" { 198 if tier != "WebServer" { 199 return fmt.Errorf("Cannont set cname_prefix for tier: %s.", tier) 200 } 201 createOpts.CNAMEPrefix = aws.String(cnamePrefix) 202 } 203 204 if tier != "" { 205 var tierType string 206 207 switch tier { 208 case "WebServer": 209 tierType = "Standard" 210 case "Worker": 211 tierType = "SQS/HTTP" 212 } 213 environmentTier := elasticbeanstalk.EnvironmentTier{ 214 Name: aws.String(tier), 215 Type: aws.String(tierType), 216 } 217 createOpts.Tier = &environmentTier 218 } 219 220 if solutionStack != "" { 221 createOpts.SolutionStackName = aws.String(solutionStack) 222 } 223 224 if templateName != "" { 225 createOpts.TemplateName = aws.String(templateName) 226 } 227 228 // Get the current time to filter describeBeanstalkEvents messages 229 t := time.Now() 230 log.Printf("[DEBUG] Elastic Beanstalk Environment create opts: %s", createOpts) 231 resp, err := conn.CreateEnvironment(&createOpts) 232 if err != nil { 233 return err 234 } 235 236 // Assign the application name as the resource ID 237 d.SetId(*resp.EnvironmentId) 238 239 stateConf := &resource.StateChangeConf{ 240 Pending: []string{"Launching", "Updating"}, 241 Target: []string{"Ready"}, 242 Refresh: environmentStateRefreshFunc(conn, d.Id()), 243 Timeout: waitForReadyTimeOut, 244 Delay: 10 * time.Second, 245 MinTimeout: 3 * time.Second, 246 } 247 248 _, err = stateConf.WaitForState() 249 if err != nil { 250 return fmt.Errorf( 251 "Error waiting for Elastic Beanstalk Environment (%s) to become ready: %s", 252 d.Id(), err) 253 } 254 255 err = describeBeanstalkEvents(conn, d.Id(), t) 256 if err != nil { 257 return err 258 } 259 260 return resourceAwsElasticBeanstalkEnvironmentRead(d, meta) 261 } 262 263 func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { 264 conn := meta.(*AWSClient).elasticbeanstalkconn 265 266 envId := d.Id() 267 waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string)) 268 if err != nil { 269 return err 270 } 271 272 updateOpts := elasticbeanstalk.UpdateEnvironmentInput{ 273 EnvironmentId: aws.String(envId), 274 } 275 276 if d.HasChange("description") { 277 updateOpts.Description = aws.String(d.Get("description").(string)) 278 } 279 280 if d.HasChange("solution_stack_name") { 281 updateOpts.SolutionStackName = aws.String(d.Get("solution_stack_name").(string)) 282 } 283 284 if d.HasChange("setting") { 285 o, n := d.GetChange("setting") 286 if o == nil { 287 o = &schema.Set{F: optionSettingValueHash} 288 } 289 if n == nil { 290 n = &schema.Set{F: optionSettingValueHash} 291 } 292 293 os := o.(*schema.Set) 294 ns := n.(*schema.Set) 295 296 updateOpts.OptionSettings = extractOptionSettings(ns.Difference(os)) 297 } 298 299 if d.HasChange("template_name") { 300 updateOpts.TemplateName = aws.String(d.Get("template_name").(string)) 301 } 302 303 // Get the current time to filter describeBeanstalkEvents messages 304 t := time.Now() 305 log.Printf("[DEBUG] Elastic Beanstalk Environment update opts: %s", updateOpts) 306 _, err = conn.UpdateEnvironment(&updateOpts) 307 if err != nil { 308 return err 309 } 310 311 stateConf := &resource.StateChangeConf{ 312 Pending: []string{"Launching", "Updating"}, 313 Target: []string{"Ready"}, 314 Refresh: environmentStateRefreshFunc(conn, d.Id()), 315 Timeout: waitForReadyTimeOut, 316 Delay: 10 * time.Second, 317 MinTimeout: 3 * time.Second, 318 } 319 320 _, err = stateConf.WaitForState() 321 if err != nil { 322 return fmt.Errorf( 323 "Error waiting for Elastic Beanstalk Environment (%s) to become ready: %s", 324 d.Id(), err) 325 } 326 327 err = describeBeanstalkEvents(conn, d.Id(), t) 328 if err != nil { 329 return err 330 } 331 332 return resourceAwsElasticBeanstalkEnvironmentRead(d, meta) 333 } 334 335 func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta interface{}) error { 336 conn := meta.(*AWSClient).elasticbeanstalkconn 337 338 app := d.Get("application").(string) 339 envId := d.Id() 340 tier := d.Get("tier").(string) 341 342 log.Printf("[DEBUG] Elastic Beanstalk environment read %s: id %s", d.Get("name").(string), d.Id()) 343 344 resp, err := conn.DescribeEnvironments(&elasticbeanstalk.DescribeEnvironmentsInput{ 345 ApplicationName: aws.String(app), 346 EnvironmentIds: []*string{aws.String(envId)}, 347 }) 348 349 if err != nil { 350 return err 351 } 352 353 if len(resp.Environments) == 0 { 354 log.Printf("[DEBUG] Elastic Beanstalk environment properties: could not find environment %s", d.Id()) 355 356 d.SetId("") 357 return nil 358 } else if len(resp.Environments) != 1 { 359 return fmt.Errorf("Error reading application properties: found %d environments, expected 1", len(resp.Environments)) 360 } 361 362 env := resp.Environments[0] 363 364 if *env.Status == "Terminated" { 365 log.Printf("[DEBUG] Elastic Beanstalk environment %s was terminated", d.Id()) 366 367 d.SetId("") 368 return nil 369 } 370 371 resources, err := conn.DescribeEnvironmentResources(&elasticbeanstalk.DescribeEnvironmentResourcesInput{ 372 EnvironmentId: aws.String(envId), 373 }) 374 375 if err != nil { 376 return err 377 } 378 379 if err := d.Set("description", env.Description); err != nil { 380 return err 381 } 382 383 if err := d.Set("cname", env.CNAME); err != nil { 384 return err 385 } 386 387 if tier == "WebServer" && env.CNAME != nil { 388 beanstalkCnamePrefixRegexp := regexp.MustCompile(`(^[^.]+).\w{2}-\w{4,9}-\d.elasticbeanstalk.com$`) 389 var cnamePrefix string 390 cnamePrefixMatch := beanstalkCnamePrefixRegexp.FindStringSubmatch(*env.CNAME) 391 392 if cnamePrefixMatch == nil { 393 cnamePrefix = "" 394 } else { 395 cnamePrefix = cnamePrefixMatch[1] 396 } 397 398 if err := d.Set("cname_prefix", cnamePrefix); err != nil { 399 return err 400 } 401 } 402 403 if err := d.Set("autoscaling_groups", flattenBeanstalkAsg(resources.EnvironmentResources.AutoScalingGroups)); err != nil { 404 return err 405 } 406 407 if err := d.Set("instances", flattenBeanstalkInstances(resources.EnvironmentResources.Instances)); err != nil { 408 return err 409 } 410 if err := d.Set("launch_configurations", flattenBeanstalkLc(resources.EnvironmentResources.LaunchConfigurations)); err != nil { 411 return err 412 } 413 if err := d.Set("load_balancers", flattenBeanstalkElb(resources.EnvironmentResources.LoadBalancers)); err != nil { 414 return err 415 } 416 if err := d.Set("queues", flattenBeanstalkSqs(resources.EnvironmentResources.Queues)); err != nil { 417 return err 418 } 419 if err := d.Set("triggers", flattenBeanstalkTrigger(resources.EnvironmentResources.Triggers)); err != nil { 420 return err 421 } 422 423 return resourceAwsElasticBeanstalkEnvironmentSettingsRead(d, meta) 424 } 425 426 func fetchAwsElasticBeanstalkEnvironmentSettings(d *schema.ResourceData, meta interface{}) (*schema.Set, error) { 427 conn := meta.(*AWSClient).elasticbeanstalkconn 428 429 app := d.Get("application").(string) 430 name := d.Get("name").(string) 431 432 resp, err := conn.DescribeConfigurationSettings(&elasticbeanstalk.DescribeConfigurationSettingsInput{ 433 ApplicationName: aws.String(app), 434 EnvironmentName: aws.String(name), 435 }) 436 437 if err != nil { 438 return nil, err 439 } 440 441 if len(resp.ConfigurationSettings) != 1 { 442 return nil, fmt.Errorf("Error reading environment settings: received %d settings groups, expected 1", len(resp.ConfigurationSettings)) 443 } 444 445 settings := &schema.Set{F: optionSettingValueHash} 446 for _, optionSetting := range resp.ConfigurationSettings[0].OptionSettings { 447 m := map[string]interface{}{} 448 449 if optionSetting.Namespace != nil { 450 m["namespace"] = *optionSetting.Namespace 451 } else { 452 return nil, fmt.Errorf("Error reading environment settings: option setting with no namespace: %v", optionSetting) 453 } 454 455 if optionSetting.OptionName != nil { 456 m["name"] = *optionSetting.OptionName 457 } else { 458 return nil, fmt.Errorf("Error reading environment settings: option setting with no name: %v", optionSetting) 459 } 460 461 if optionSetting.Value != nil { 462 switch *optionSetting.OptionName { 463 case "SecurityGroups": 464 m["value"] = dropGeneratedSecurityGroup(*optionSetting.Value, meta) 465 case "Subnets", "ELBSubnets": 466 m["value"] = sortValues(*optionSetting.Value) 467 default: 468 m["value"] = *optionSetting.Value 469 } 470 } 471 472 settings.Add(m) 473 } 474 475 return settings, nil 476 } 477 478 func resourceAwsElasticBeanstalkEnvironmentSettingsRead(d *schema.ResourceData, meta interface{}) error { 479 log.Printf("[DEBUG] Elastic Beanstalk environment settings read %s: id %s", d.Get("name").(string), d.Id()) 480 481 allSettings, err := fetchAwsElasticBeanstalkEnvironmentSettings(d, meta) 482 if err != nil { 483 return err 484 } 485 486 settings := d.Get("setting").(*schema.Set) 487 488 log.Printf("[DEBUG] Elastic Beanstalk allSettings: %s", allSettings.GoString()) 489 log.Printf("[DEBUG] Elastic Beanstalk settings: %s", settings.GoString()) 490 491 // perform the set operation with only name/namespace as keys, excluding value 492 // this is so we override things in the settings resource data key with updated values 493 // from the api. we skip values we didn't know about before because there are so many 494 // defaults set by the eb api that we would delete many useful defaults. 495 // 496 // there is likely a better way to do this 497 allSettingsKeySet := schema.NewSet(optionSettingKeyHash, allSettings.List()) 498 settingsKeySet := schema.NewSet(optionSettingKeyHash, settings.List()) 499 updatedSettingsKeySet := allSettingsKeySet.Intersection(settingsKeySet) 500 501 log.Printf("[DEBUG] Elastic Beanstalk updatedSettingsKeySet: %s", updatedSettingsKeySet.GoString()) 502 503 updatedSettings := schema.NewSet(optionSettingValueHash, updatedSettingsKeySet.List()) 504 505 log.Printf("[DEBUG] Elastic Beanstalk updatedSettings: %s", updatedSettings.GoString()) 506 507 if err := d.Set("all_settings", allSettings.List()); err != nil { 508 return err 509 } 510 511 if err := d.Set("setting", updatedSettings.List()); err != nil { 512 return err 513 } 514 515 return nil 516 } 517 518 func resourceAwsElasticBeanstalkEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { 519 conn := meta.(*AWSClient).elasticbeanstalkconn 520 521 waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string)) 522 if err != nil { 523 return err 524 } 525 526 opts := elasticbeanstalk.TerminateEnvironmentInput{ 527 EnvironmentId: aws.String(d.Id()), 528 TerminateResources: aws.Bool(true), 529 } 530 531 // Get the current time to filter describeBeanstalkEvents messages 532 t := time.Now() 533 log.Printf("[DEBUG] Elastic Beanstalk Environment terminate opts: %s", opts) 534 _, err = conn.TerminateEnvironment(&opts) 535 536 if err != nil { 537 return err 538 } 539 540 stateConf := &resource.StateChangeConf{ 541 Pending: []string{"Terminating"}, 542 Target: []string{"Terminated"}, 543 Refresh: environmentStateRefreshFunc(conn, d.Id()), 544 Timeout: waitForReadyTimeOut, 545 Delay: 10 * time.Second, 546 MinTimeout: 3 * time.Second, 547 } 548 549 _, err = stateConf.WaitForState() 550 if err != nil { 551 return fmt.Errorf( 552 "Error waiting for Elastic Beanstalk Environment (%s) to become terminated: %s", 553 d.Id(), err) 554 } 555 556 err = describeBeanstalkEvents(conn, d.Id(), t) 557 if err != nil { 558 return err 559 } 560 561 return nil 562 } 563 564 // environmentStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 565 // the creation of the Beanstalk Environment 566 func environmentStateRefreshFunc(conn *elasticbeanstalk.ElasticBeanstalk, environmentId string) resource.StateRefreshFunc { 567 return func() (interface{}, string, error) { 568 resp, err := conn.DescribeEnvironments(&elasticbeanstalk.DescribeEnvironmentsInput{ 569 EnvironmentIds: []*string{aws.String(environmentId)}, 570 }) 571 if err != nil { 572 log.Printf("[Err] Error waiting for Elastic Beanstalk Environment state: %s", err) 573 return -1, "failed", fmt.Errorf("[Err] Error waiting for Elastic Beanstalk Environment state: %s", err) 574 } 575 576 if resp == nil || len(resp.Environments) == 0 { 577 // Sometimes AWS just has consistency issues and doesn't see 578 // our instance yet. Return an empty state. 579 return nil, "", nil 580 } 581 582 var env *elasticbeanstalk.EnvironmentDescription 583 for _, e := range resp.Environments { 584 if environmentId == *e.EnvironmentId { 585 env = e 586 } 587 } 588 589 if env == nil { 590 return -1, "failed", fmt.Errorf("[Err] Error finding Elastic Beanstalk Environment, environment not found") 591 } 592 593 return env, *env.Status, nil 594 } 595 } 596 597 // we use the following two functions to allow us to split out defaults 598 // as they become overridden from within the template 599 func optionSettingValueHash(v interface{}) int { 600 rd := v.(map[string]interface{}) 601 namespace := rd["namespace"].(string) 602 optionName := rd["name"].(string) 603 value, _ := rd["value"].(string) 604 hk := fmt.Sprintf("%s:%s=%s", namespace, optionName, sortValues(value)) 605 log.Printf("[DEBUG] Elastic Beanstalk optionSettingValueHash(%#v): %s: hk=%s,hc=%d", v, optionName, hk, hashcode.String(hk)) 606 return hashcode.String(hk) 607 } 608 609 func optionSettingKeyHash(v interface{}) int { 610 rd := v.(map[string]interface{}) 611 namespace := rd["namespace"].(string) 612 optionName := rd["name"].(string) 613 hk := fmt.Sprintf("%s:%s", namespace, optionName) 614 log.Printf("[DEBUG] Elastic Beanstalk optionSettingKeyHash(%#v): %s: hk=%s,hc=%d", v, optionName, hk, hashcode.String(hk)) 615 return hashcode.String(hk) 616 } 617 618 func sortValues(v string) string { 619 values := strings.Split(v, ",") 620 sort.Strings(values) 621 return strings.Join(values, ",") 622 } 623 624 func extractOptionSettings(s *schema.Set) []*elasticbeanstalk.ConfigurationOptionSetting { 625 settings := []*elasticbeanstalk.ConfigurationOptionSetting{} 626 627 if s != nil { 628 for _, setting := range s.List() { 629 settings = append(settings, &elasticbeanstalk.ConfigurationOptionSetting{ 630 Namespace: aws.String(setting.(map[string]interface{})["namespace"].(string)), 631 OptionName: aws.String(setting.(map[string]interface{})["name"].(string)), 632 Value: aws.String(setting.(map[string]interface{})["value"].(string)), 633 }) 634 } 635 } 636 637 return settings 638 } 639 640 func dropGeneratedSecurityGroup(settingValue string, meta interface{}) string { 641 conn := meta.(*AWSClient).ec2conn 642 643 groups := strings.Split(settingValue, ",") 644 645 resp, err := conn.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ 646 GroupIds: aws.StringSlice(groups), 647 }) 648 649 if err != nil { 650 log.Printf("[DEBUG] Elastic Beanstalk error describing SecurityGroups: %v", err) 651 return settingValue 652 } 653 654 var legitGroups []string 655 for _, group := range resp.SecurityGroups { 656 log.Printf("[DEBUG] Elastic Beanstalk SecurityGroup: %v", *group.GroupName) 657 if !strings.HasPrefix(*group.GroupName, "awseb") { 658 legitGroups = append(legitGroups, *group.GroupId) 659 } 660 } 661 662 sort.Strings(legitGroups) 663 664 return strings.Join(legitGroups, ",") 665 } 666 667 func describeBeanstalkEvents(conn *elasticbeanstalk.ElasticBeanstalk, environmentId string, t time.Time) error { 668 beanstalkErrors, err := conn.DescribeEvents(&elasticbeanstalk.DescribeEventsInput{ 669 EnvironmentId: aws.String(environmentId), 670 Severity: aws.String("ERROR"), 671 StartTime: aws.Time(t), 672 }) 673 674 if err != nil { 675 log.Printf("[Err] Unable to get Elastic Beanstalk Evironment events: %s", err) 676 } 677 678 events := "" 679 for _, event := range beanstalkErrors.Events { 680 events = events + "\n" + event.EventDate.String() + ": " + *event.Message 681 } 682 683 if events != "" { 684 return fmt.Errorf("%s", events) 685 } 686 687 return nil 688 }