diff --git a/.changelog/12433.txt b/.changelog/12433.txt new file mode 100644 index 00000000000..7b0a5da8658 --- /dev/null +++ b/.changelog/12433.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_opsworks_custom_layer: Add support for `cloudwatch_configuration` +``` + +```release-note:enhancement +resource/aws_opsworks_custom_layer: Add plan time validation for `ebs_volume.type` and `custom_json`. +``` \ No newline at end of file diff --git a/internal/service/opsworks/custom_layer_test.go b/internal/service/opsworks/custom_layer_test.go index fccac324f43..8e5ed20abcd 100644 --- a/internal/service/opsworks/custom_layer_test.go +++ b/internal/service/opsworks/custom_layer_test.go @@ -2,7 +2,6 @@ package opsworks_test import ( "fmt" - "reflect" "testing" "github.com/aws/aws-sdk-go/aws" @@ -13,15 +12,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfopsworks "github.com/hashicorp/terraform-provider-aws/internal/service/opsworks" ) // These tests assume the existence of predefined Opsworks IAM roles named `aws-opsworks-ec2-role` // and `aws-opsworks-service-role`, and Opsworks stacks named `tf-acc`. func TestAccOpsWorksCustomLayer_basic(t *testing.T) { - name := sdkacctest.RandString(10) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) var opslayer opsworks.Layer - resourceName := "aws_opsworks_custom_layer.tf-acc" + resourceName := "aws_opsworks_custom_layer.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(opsworks.EndpointsID, t) }, @@ -30,10 +30,10 @@ func TestAccOpsWorksCustomLayer_basic(t *testing.T) { CheckDestroy: testAccCheckCustomLayerDestroy, Steps: []resource.TestStep{ { - Config: testAccCustomLayerVPCCreateConfig(name), + Config: testAccCustomLayerVPCCreateConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckLayerExists(resourceName, &opslayer), - resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "auto_assign_elastic_ips", "false"), resource.TestCheckResourceAttr(resourceName, "auto_healing", "true"), resource.TestCheckResourceAttr(resourceName, "drain_elb_on_shutdown", "true"), @@ -62,7 +62,7 @@ func TestAccOpsWorksCustomLayer_basic(t *testing.T) { } func TestAccOpsWorksCustomLayer_tags(t *testing.T) { - name := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) var opslayer opsworks.Layer resourceName := "aws_opsworks_custom_layer.test" @@ -73,7 +73,7 @@ func TestAccOpsWorksCustomLayer_tags(t *testing.T) { CheckDestroy: testAccCheckCustomLayerDestroy, Steps: []resource.TestStep{ { - Config: testAccCustomLayerTags1Config(name, "key1", "value1"), + Config: testAccCustomLayerTags1Config(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckLayerExists(resourceName, &opslayer), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -86,7 +86,7 @@ func TestAccOpsWorksCustomLayer_tags(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccCustomLayerTags2Config(name, "key1", "value1updated", "key2", "value2"), + Config: testAccCustomLayerTags2Config(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckLayerExists(resourceName, &opslayer), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), @@ -95,7 +95,7 @@ func TestAccOpsWorksCustomLayer_tags(t *testing.T) { ), }, { - Config: testAccCustomLayerTags1Config(name, "key2", "value2"), + Config: testAccCustomLayerTags1Config(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckLayerExists(resourceName, &opslayer), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -107,9 +107,9 @@ func TestAccOpsWorksCustomLayer_tags(t *testing.T) { } func TestAccOpsWorksCustomLayer_noVPC(t *testing.T) { - stackName := fmt.Sprintf("tf-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) var opslayer opsworks.Layer - resourceName := "aws_opsworks_custom_layer.tf-acc" + resourceName := "aws_opsworks_custom_layer.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(opsworks.EndpointsID, t) }, @@ -118,11 +118,10 @@ func TestAccOpsWorksCustomLayer_noVPC(t *testing.T) { CheckDestroy: testAccCheckCustomLayerDestroy, Steps: []resource.TestStep{ { - Config: testAccCustomLayerNoVPCCreateConfig(stackName), + Config: testAccCustomLayerNoVPCCreateConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckLayerExists(resourceName, &opslayer), - testAccCheckCreateLayerAttributes(&opslayer, stackName), - resource.TestCheckResourceAttr(resourceName, "name", stackName), + resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "auto_assign_elastic_ips", "false"), resource.TestCheckResourceAttr(resourceName, "auto_healing", "true"), resource.TestCheckResourceAttr(resourceName, "drain_elb_on_shutdown", "true"), @@ -142,9 +141,14 @@ func TestAccOpsWorksCustomLayer_noVPC(t *testing.T) { ), }, { - Config: testAccCustomLayerUpdateConfig(stackName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccCustomLayerUpdateConfig(rName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", stackName), + resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "drain_elb_on_shutdown", "false"), resource.TestCheckResourceAttr(resourceName, "instance_shutdown_timeout", "120"), resource.TestCheckResourceAttr(resourceName, "custom_security_group_ids.#", "3"), @@ -175,6 +179,94 @@ func TestAccOpsWorksCustomLayer_noVPC(t *testing.T) { }) } +func TestAccOpsWorksCustomLayer_cloudwatch(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + var opslayer opsworks.Layer + resourceName := "aws_opsworks_custom_layer.test" + logGroupResourceName := "aws_cloudwatch_log_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(opsworks.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, opsworks.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckCustomLayerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOpsworksCustomLayerConfigCloudWatch(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckLayerExists(resourceName, &opslayer), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "cloudwatch_configuration.0.log_streams.0.log_group_name", logGroupResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.file", "/var/log/system.log*"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccOpsworksCustomLayerConfigCloudWatch(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckLayerExists(resourceName, &opslayer), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "cloudwatch_configuration.0.log_streams.0.log_group_name", logGroupResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.file", "/var/log/system.log*"), + ), + }, + { + Config: testAccOpsworksCustomLayerConfigCloudWatchFull(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckLayerExists(resourceName, &opslayer), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "cloudwatch_configuration.0.log_streams.0.log_group_name", logGroupResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.file", "/var/log/system.lo*"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.batch_count", "2000"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.batch_size", "50000"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.buffer_duration", "6000"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.encoding", "mac_turkish"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.file_fingerprint_lines", "2"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.initial_position", "end_of_file"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.multiline_start_pattern", "test*"), + resource.TestCheckResourceAttr(resourceName, "cloudwatch_configuration.0.log_streams.0.time_zone", "LOCAL"), + ), + }, + }, + }) +} + +func TestAccOpsWorksCustomLayer_disappears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + var opslayer opsworks.Layer + resourceName := "aws_opsworks_custom_layer.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(opsworks.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, opsworks.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckCustomLayerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCustomLayerNoVPCCreateConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckLayerExists(resourceName, &opslayer), + acctest.CheckResourceDisappears(acctest.Provider, tfopsworks.ResourceCustomLayer(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckLayerExists(n string, opslayer *opsworks.Layer) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -207,67 +299,6 @@ func testAccCheckLayerExists(n string, opslayer *opsworks.Layer) resource.TestCh } } -func testAccCheckCreateLayerAttributes( - opslayer *opsworks.Layer, stackName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *opslayer.Name != stackName { - return fmt.Errorf("Unexpected name: %s", *opslayer.Name) - } - - if *opslayer.AutoAssignElasticIps { - return fmt.Errorf( - "Unexpected AutoAssignElasticIps: %t", *opslayer.AutoAssignElasticIps) - } - - if !*opslayer.EnableAutoHealing { - return fmt.Errorf( - "Unexpected EnableAutoHealing: %t", *opslayer.EnableAutoHealing) - } - - if !*opslayer.LifecycleEventConfiguration.Shutdown.DelayUntilElbConnectionsDrained { - return fmt.Errorf( - "Unexpected DelayUntilElbConnectionsDrained: %t", - *opslayer.LifecycleEventConfiguration.Shutdown.DelayUntilElbConnectionsDrained) - } - - if *opslayer.LifecycleEventConfiguration.Shutdown.ExecutionTimeout != 300 { - return fmt.Errorf( - "Unexpected ExecutionTimeout: %d", - *opslayer.LifecycleEventConfiguration.Shutdown.ExecutionTimeout) - } - - if v := len(opslayer.CustomSecurityGroupIds); v != 2 { - return fmt.Errorf("Expected 2 customSecurityGroupIds, got %d", v) - } - - expectedPackages := []*string{ - aws.String("git"), - aws.String("golang"), - } - - if !reflect.DeepEqual(expectedPackages, opslayer.Packages) { - return fmt.Errorf("Unexpected Packages: %v", aws.StringValueSlice(opslayer.Packages)) - } - - expectedEbsVolumes := []*opsworks.VolumeConfiguration{ - { - Encrypted: aws.Bool(false), - MountPoint: aws.String("/home"), - NumberOfDisks: aws.Int64(2), - RaidLevel: aws.Int64(0), - Size: aws.Int64(100), - VolumeType: aws.String("gp2"), - }, - } - - if !reflect.DeepEqual(expectedEbsVolumes, opslayer.VolumeConfigurations) { - return fmt.Errorf("Unnexpected VolumeConfiguration: %s", opslayer.VolumeConfigurations) - } - - return nil - } -} - func testAccCheckLayerDestroy(resourceType string, s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).OpsWorksConn for _, rs := range s.RootModule().Resources { @@ -280,16 +311,22 @@ func testAccCheckLayerDestroy(resourceType string, s *terraform.State) error { }, } - _, err := conn.DescribeLayers(req) + output, err := conn.DescribeLayers(req) + if tfawserr.ErrCodeEquals(err, opsworks.ErrCodeResourceNotFoundException) { + continue + } if err != nil { - if tfawserr.ErrMessageContains(err, opsworks.ErrCodeResourceNotFoundException, "") { - return nil - } return err } + + if output == nil || len(output.Layers) == 0 { + return nil + } + + return fmt.Errorf("OpsWorks layer %q still exists", rs.Primary.ID) } - return fmt.Errorf("Fall through error on OpsWorks layer test") + return nil } func testAccCheckCustomLayerDestroy(s *terraform.State) error { @@ -299,7 +336,7 @@ func testAccCheckCustomLayerDestroy(s *terraform.State) error { func testAccCustomLayerSecurityGroups(name string) string { return fmt.Sprintf(` resource "aws_security_group" "tf-ops-acc-layer1" { - name = "%s-layer1" + name = "%[1]s-layer1" ingress { from_port = 8 @@ -310,7 +347,7 @@ resource "aws_security_group" "tf-ops-acc-layer1" { } resource "aws_security_group" "tf-ops-acc-layer2" { - name = "%s-layer2" + name = "%[1]s-layer2" ingress { from_port = 8 @@ -319,12 +356,14 @@ resource "aws_security_group" "tf-ops-acc-layer2" { cidr_blocks = ["0.0.0.0/0"] } } -`, name, name) +`, name) } func testAccCustomLayerNoVPCCreateConfig(name string) string { - return fmt.Sprintf(` -resource "aws_opsworks_custom_layer" "tf-acc" { + return testAccStackNoVPCCreateConfig(name) + + testAccCustomLayerSecurityGroups(name) + + fmt.Sprintf(` +resource "aws_opsworks_custom_layer" "test" { stack_id = aws_opsworks_stack.tf-acc.id name = "%s" short_name = "tf-ops-acc-custom-layer" @@ -349,16 +388,14 @@ resource "aws_opsworks_custom_layer" "tf-acc" { encrypted = false } } - -%s - -%s -`, name, testAccStackNoVPCCreateConfig(name), testAccCustomLayerSecurityGroups(name)) +`, name) } func testAccCustomLayerVPCCreateConfig(name string) string { - return fmt.Sprintf(` -resource "aws_opsworks_custom_layer" "tf-acc" { + return testAccStackNoVPCCreateConfig(name) + + testAccCustomLayerSecurityGroups(name) + + fmt.Sprintf(` +resource "aws_opsworks_custom_layer" "test" { stack_id = aws_opsworks_stack.tf-acc.id name = "%s" short_name = "tf-ops-acc-custom-layer" @@ -385,15 +422,13 @@ resource "aws_opsworks_custom_layer" "tf-acc" { raid_level = 0 } } - -%s - -%s -`, name, testAccStackVPCCreateConfig(name), testAccCustomLayerSecurityGroups(name)) +`, name) } func testAccCustomLayerUpdateConfig(name string) string { - return fmt.Sprintf(` + return testAccStackNoVPCCreateConfig(name) + + testAccCustomLayerSecurityGroups(name) + + fmt.Sprintf(` resource "aws_security_group" "tf-ops-acc-layer3" { name = "tf-ops-acc-layer-%[1]s" @@ -405,7 +440,7 @@ resource "aws_security_group" "tf-ops-acc-layer3" { } } -resource "aws_opsworks_custom_layer" "tf-acc" { +resource "aws_opsworks_custom_layer" "test" { stack_id = aws_opsworks_stack.tf-acc.id name = "%[1]s" short_name = "tf-ops-acc-custom-layer" @@ -448,11 +483,7 @@ resource "aws_opsworks_custom_layer" "tf-acc" { } EOF } - -%s - -%s -`, name, testAccStackNoVPCCreateConfig(name), testAccCustomLayerSecurityGroups(name)) +`, name) } func testAccCustomLayerTags1Config(name, tagKey1, tagValue1 string) string { @@ -531,3 +562,69 @@ resource "aws_opsworks_custom_layer" "test" { } `, name, tagKey1, tagValue1, tagKey2, tagValue2) } + +func testAccOpsworksCustomLayerConfigCloudWatch(name string, enabled bool) string { + return testAccStackNoVPCCreateConfig(name) + + testAccCustomLayerSecurityGroups(name) + + fmt.Sprintf(` +resource "aws_cloudwatch_log_group" "test" { + name = %[1]q +} +resource "aws_opsworks_custom_layer" "test" { + stack_id = aws_opsworks_stack.tf-acc.id + name = %[1]q + short_name = "tf-ops-acc-custom-layer" + auto_assign_public_ips = true + custom_security_group_ids = [ + aws_security_group.tf-ops-acc-layer1.id, + aws_security_group.tf-ops-acc-layer2.id, + ] + drain_elb_on_shutdown = true + instance_shutdown_timeout = 300 + cloudwatch_configuration { + enabled = %[2]t + log_streams { + log_group_name = aws_cloudwatch_log_group.test.name + file = "/var/log/system.log*" + } + } +} +`, name, enabled) +} + +func testAccOpsworksCustomLayerConfigCloudWatchFull(name string) string { + return testAccStackNoVPCCreateConfig(name) + + testAccCustomLayerSecurityGroups(name) + + fmt.Sprintf(` +resource "aws_cloudwatch_log_group" "test" { + name = %[1]q +} +resource "aws_opsworks_custom_layer" "test" { + stack_id = aws_opsworks_stack.tf-acc.id + name = %[1]q + short_name = "tf-ops-acc-custom-layer" + auto_assign_public_ips = true + custom_security_group_ids = [ + aws_security_group.tf-ops-acc-layer1.id, + aws_security_group.tf-ops-acc-layer2.id, + ] + drain_elb_on_shutdown = true + instance_shutdown_timeout = 300 + cloudwatch_configuration { + enabled = true + log_streams { + log_group_name = aws_cloudwatch_log_group.test.name + file = "/var/log/system.lo*" + batch_count = 2000 + batch_size = 50000 + buffer_duration = 6000 + encoding = "mac_turkish" + file_fingerprint_lines = "2" + initial_position = "end_of_file" + multiline_start_pattern = "test*" + time_zone = "LOCAL" + } + } +} +`, name) +} diff --git a/internal/service/opsworks/layers.go b/internal/service/opsworks/layers.go index bcfa3bf7982..5c46b3256a9 100644 --- a/internal/service/opsworks/layers.go +++ b/internal/service/opsworks/layers.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/flex" @@ -50,157 +51,209 @@ var ( func (lt *opsworksLayerType) SchemaResource() *schema.Resource { resourceSchema := map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "auto_assign_elastic_ips": { Type: schema.TypeBool, Optional: true, Default: false, }, - "auto_assign_public_ips": { Type: schema.TypeBool, Optional: true, Default: false, }, - + "auto_healing": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "cloudwatch_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "1" && new == "0" && !d.Get("cloudwatch_configuration.0.enabled").(bool) { + return true + } + return false + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "false" && new == "" { + return true + } + return false + }, + }, + "log_streams": { + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "1" && new == "0" && !d.Get("cloudwatch_configuration.0.enabled").(bool) { + return true + } + return false + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "batch_count": { + Type: schema.TypeInt, + Default: 1000, + Optional: true, + ValidateFunc: validation.IntAtMost(10000), + }, + "batch_size": { + Type: schema.TypeInt, + Default: 32768, + Optional: true, + ValidateFunc: validation.IntAtMost(1048576), + }, + "buffer_duration": { + Type: schema.TypeInt, + Default: 5000, + Optional: true, + ValidateFunc: validation.IntAtLeast(5000), + }, + "datetime_format": { + Type: schema.TypeString, + Optional: true, + }, + "encoding": { + Type: schema.TypeString, + Optional: true, + Default: opsworks.CloudWatchLogsEncodingUtf8, + ValidateFunc: validation.StringInSlice(opsworks.CloudWatchLogsEncoding_Values(), false), + }, + "file": { + Type: schema.TypeString, + Required: true, + }, + "file_fingerprint_lines": { + Type: schema.TypeString, + Optional: true, + Default: "1", + }, + "initial_position": { + Type: schema.TypeString, + Optional: true, + Default: opsworks.CloudWatchLogsInitialPositionStartOfFile, + ValidateFunc: validation.StringInSlice(opsworks.CloudWatchLogsInitialPosition_Values(), false), + }, + "log_group_name": { + Type: schema.TypeString, + Required: true, + }, + "multiline_start_pattern": { + Type: schema.TypeString, + Optional: true, + }, + "time_zone": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(opsworks.CloudWatchLogsTimeZone_Values(), false), + }, + }, + }, + }, + }, + }, + }, "custom_instance_profile_arn": { Type: schema.TypeString, Optional: true, ValidateFunc: verify.ValidARN, }, - - "elastic_load_balancer": { - Type: schema.TypeString, - Optional: true, - }, - "custom_setup_recipes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "custom_configure_recipes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "custom_deploy_recipes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "custom_undeploy_recipes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "custom_shutdown_recipes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "custom_security_group_ids": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, - "custom_json": { - Type: schema.TypeString, + Type: schema.TypeString, + ValidateFunc: validation.StringIsJSON, StateFunc: func(v interface{}) string { json, _ := structure.NormalizeJsonString(v) return json }, Optional: true, }, - - "auto_healing": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "install_updates_on_boot": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "instance_shutdown_timeout": { - Type: schema.TypeInt, - Optional: true, - Default: 120, - }, - "drain_elb_on_shutdown": { Type: schema.TypeBool, Optional: true, Default: true, }, - - "system_packages": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, - }, - - "stack_id": { - Type: schema.TypeString, - ForceNew: true, - Required: true, - }, - - "use_ebs_optimized_instances": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "ebs_volume": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "iops": { Type: schema.TypeInt, Optional: true, Default: 0, }, - "mount_point": { Type: schema.TypeString, Required: true, }, - "number_of_disks": { Type: schema.TypeInt, Required: true, }, - "raid_level": { Type: schema.TypeString, Optional: true, Default: "", }, - "size": { Type: schema.TypeInt, Required: true, }, - "type": { Type: schema.TypeString, Optional: true, Default: "standard", + ValidateFunc: validation.StringInSlice([]string{ + "standard", + "io1", + "gp2", + "st1", + "sc1", + }, false), }, - "encrypted": { Type: schema.TypeBool, Optional: true, @@ -213,9 +266,35 @@ func (lt *opsworksLayerType) SchemaResource() *schema.Resource { return create.StringHashcode(m["mount_point"].(string)) }, }, - "arn": { + "elastic_load_balancer": { Type: schema.TypeString, - Computed: true, + Optional: true, + }, + "install_updates_on_boot": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "instance_shutdown_timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 120, + }, + "system_packages": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "stack_id": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "use_ebs_optimized_instances": { + Type: schema.TypeBool, + Optional: true, + Default: false, }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), @@ -307,6 +386,9 @@ func (lt *opsworksLayerType) Read(d *schema.ResourceData, meta interface{}) erro d.Set("system_packages", flex.FlattenStringList(layer.Packages)) d.Set("stack_id", layer.StackId) d.Set("use_ebs_optimized_instances", layer.UseEbsOptimizedInstances) + if err := d.Set("cloudwatch_configuration", flattenOpsworksCloudWatchConfig(layer.CloudWatchLogsConfiguration)); err != nil { + return fmt.Errorf("error setting cloudwatch_configuration: %w", err) + } if lt.CustomShortName { d.Set("short_name", layer.Shortname) @@ -399,6 +481,10 @@ func (lt *opsworksLayerType) Create(d *schema.ResourceData, meta interface{}) er VolumeConfigurations: lt.VolumeConfigurations(d), } + if v, ok := d.GetOk("cloudwatch_configuration"); ok { + req.CloudWatchLogsConfiguration = expandOpsworksCloudWatchConfig(v.([]interface{})) + } + if lt.CustomShortName { req.Shortname = aws.String(d.Get("short_name").(string)) } else { @@ -470,6 +556,10 @@ func (lt *opsworksLayerType) Update(d *schema.ResourceData, meta interface{}) er VolumeConfigurations: lt.VolumeConfigurations(d), } + if v, ok := d.GetOk("cloudwatch_configuration"); ok { + req.CloudWatchLogsConfiguration = expandOpsworksCloudWatchConfig(v.([]interface{})) + } + if lt.CustomShortName { req.Shortname = aws.String(d.Get("short_name").(string)) } else { @@ -724,3 +814,148 @@ func (lt *opsworksLayerType) SetVolumeConfigurations(d *schema.ResourceData, v [ d.Set("ebs_volume", newValue) } + +func expandOpsworksCloudWatchConfig(l []interface{}) *opsworks.CloudWatchLogsConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &opsworks.CloudWatchLogsConfiguration{ + Enabled: aws.Bool(m["enabled"].(bool)), + LogStreams: expandOpsworksCloudWatchConfigLogStream(m["log_streams"].([]interface{})), + } + + return config +} + +func expandOpsworksCloudWatchConfigLogStream(l []interface{}) []*opsworks.CloudWatchLogsLogStream { + if len(l) == 0 || l[0] == nil { + return nil + } + + logStreams := make([]*opsworks.CloudWatchLogsLogStream, 0) + + for _, m := range l { + item := m.(map[string]interface{}) + logStream := &opsworks.CloudWatchLogsLogStream{} + + if v, ok := item["batch_count"]; ok { + logStream.BatchCount = aws.Int64(int64(v.(int))) + } + + if v, ok := item["batch_size"]; ok { + logStream.BatchSize = aws.Int64(int64(v.(int))) + } + + if v, ok := item["buffer_duration"]; ok { + logStream.BufferDuration = aws.Int64(int64(v.(int))) + } + + if v, ok := item["datetime_format"]; ok { + logStream.DatetimeFormat = aws.String(v.(string)) + } + + if v, ok := item["encoding"]; ok { + logStream.Encoding = aws.String(v.(string)) + } + + if v, ok := item["file"]; ok { + logStream.File = aws.String(v.(string)) + } + + if v, ok := item["file_fingerprint_lines"]; ok { + logStream.FileFingerprintLines = aws.String(v.(string)) + } + + if v, ok := item["initial_position"]; ok { + logStream.InitialPosition = aws.String(v.(string)) + } + + if v, ok := item["log_group_name"]; ok { + logStream.LogGroupName = aws.String(v.(string)) + } + + if v, ok := item["multiline_start_pattern"]; ok { + logStream.MultiLineStartPattern = aws.String(v.(string)) + } + + if v, ok := item["time_zone"]; ok { + logStream.TimeZone = aws.String(v.(string)) + } + + logStreams = append(logStreams, logStream) + } + + return logStreams +} + +func flattenOpsworksCloudWatchConfig(cloudwatchConfig *opsworks.CloudWatchLogsConfiguration) []map[string]interface{} { + if cloudwatchConfig == nil { + return nil + } + + p := map[string]interface{}{ + "enabled": aws.BoolValue(cloudwatchConfig.Enabled), + "log_streams": flattenOpsworksCloudWatchConfigLogStreams(cloudwatchConfig.LogStreams), + } + + return []map[string]interface{}{p} +} + +func flattenOpsworksCloudWatchConfigLogStreams(logStreams []*opsworks.CloudWatchLogsLogStream) []interface{} { + out := make([]interface{}, len(logStreams)) + + for i, logStream := range logStreams { + m := make(map[string]interface{}) + + if logStream.TimeZone != nil { + m["time_zone"] = aws.StringValue(logStream.TimeZone) + } + + if logStream.MultiLineStartPattern != nil { + m["multiline_start_pattern"] = aws.StringValue(logStream.MultiLineStartPattern) + } + + if logStream.Encoding != nil { + m["encoding"] = aws.StringValue(logStream.Encoding) + } + + if logStream.LogGroupName != nil { + m["log_group_name"] = aws.StringValue(logStream.LogGroupName) + } + + if logStream.File != nil { + m["file"] = aws.StringValue(logStream.File) + } + + if logStream.DatetimeFormat != nil { + m["datetime_format"] = aws.StringValue(logStream.DatetimeFormat) + } + + if logStream.FileFingerprintLines != nil { + m["file_fingerprint_lines"] = aws.StringValue(logStream.FileFingerprintLines) + } + + if logStream.InitialPosition != nil { + m["initial_position"] = aws.StringValue(logStream.InitialPosition) + } + + if logStream.BatchSize != nil { + m["batch_size"] = aws.Int64Value(logStream.BatchSize) + } + + if logStream.BatchCount != nil { + m["batch_count"] = aws.Int64Value(logStream.BatchCount) + } + + if logStream.BufferDuration != nil { + m["buffer_duration"] = aws.Int64Value(logStream.BufferDuration) + } + + out[i] = m + } + + return out +} diff --git a/website/docs/r/opsworks_custom_layer.html.markdown b/website/docs/r/opsworks_custom_layer.html.markdown index aed0e901f81..286bd3010c3 100644 --- a/website/docs/r/opsworks_custom_layer.html.markdown +++ b/website/docs/r/opsworks_custom_layer.html.markdown @@ -29,6 +29,7 @@ The following arguments are supported: * `stack_id` - (Required) The id of the stack the layer will belong to. * `auto_assign_elastic_ips` - (Optional) Whether to automatically assign an elastic IP address to the layer's instances. * `auto_assign_public_ips` - (Optional) For stacks belonging to a VPC, whether to automatically assign a public IP address to each of the layer's instances. +* `cloudwatch_configuration` - (Optional) Will create an EBS volume and connect it to the layer's instances. See [Cloudwatch Configuration](#cloudwatch-configuration). * `custom_instance_profile_arn` - (Optional) The ARN of an IAM profile that will be used for the layer's instances. * `custom_security_group_ids` - (Optional) Ids for a set of security groups to apply to the layer's instances. * `auto_healing` - (Optional) Whether to enable auto-healing for the layer. @@ -38,7 +39,7 @@ The following arguments are supported: * `drain_elb_on_shutdown` - (Optional) Whether to enable Elastic Load Balancing connection draining. * `system_packages` - (Optional) Names of a set of system packages to install on the layer's instances. * `use_ebs_optimized_instances` - (Optional) Whether to use EBS-optimized instances. -* `ebs_volume` - (Optional) `ebs_volume` blocks, as described below, will each create an EBS volume and connect it to the layer's instances. +* `ebs_volume` - (Optional) Will create an EBS volume and connect it to the layer's instances. See [EBS Volume](#ebs-volume). * `custom_json` - (Optional) Custom JSON attributes to apply to the layer. * `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. @@ -52,7 +53,7 @@ lifecycle events, if custom cookbooks are enabled on the layer's stack: * `custom_shutdown_recipes` * `custom_undeploy_recipes` -An `ebs_volume` block supports the following arguments: +### EBS Volume * `mount_point` - (Required) The path to mount the EBS volume on the layer's instances. * `size` - (Required) The size of the volume in gigabytes. @@ -62,6 +63,26 @@ An `ebs_volume` block supports the following arguments: * `iops` - (Optional) For PIOPS volumes, the IOPS per disk. * `encrypted` - (Optional) Encrypt the volume. +### Cloudwatch Configuration + +* `enabled` - (Optional) +* `log_streams` - (Optional) A block the specifies how an opsworks logs look like. See [Log Streams](#log-streams). + +#### Log Streams + +* `file` - (Required) Specifies log files that you want to push to CloudWatch Logs. File can point to a specific file or multiple files (by using wild card characters such as /var/log/system.log*). +* `log_group_name` - (Required) Specifies the destination log group. A log group is created automatically if it doesn't already exist. +* `batch_count` - (Optional) Specifies the max number of log events in a batch, up to `10000`. The default value is `1000`. +* `batch_size` - (Optional) Specifies the maximum size of log events in a batch, in bytes, up to `1048576` bytes. The default value is `32768` bytes. +* `buffer_duration` - (Optional) Specifies the time duration for the batching of log events. The minimum value is `5000` and default value is `5000`. +* `datetime_format` - (Optional) Specifies how the timestamp is extracted from logs. For more information, see the CloudWatch Logs Agent Reference (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html). +* `encoding` - (Optional) Specifies the encoding of the log file so that the file can be read correctly. The default is `utf_8`. +* `file_fingerprint_lines` - (Optional) Specifies the range of lines for identifying a file. The valid values are one number, or two dash-delimited numbers, such as `1`, `2-5`. The default value is `1`. +* `initial_position` - (Optional) Specifies where to start to read data (`start_of_file` or `end_of_file`). The default is `start_of_file`. +* `multiline_start_pattern` - (Optional) Specifies the pattern for identifying the start of a log message. +* `time_zone` - (Optional) Specifies the time zone of log event time stamps. + + ## Attributes Reference In addition to all arguments above, the following attributes are exported: