Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

r/aws_opsworks_custom_layer - add cloudwatch configuration + disappears test #12433

Merged
merged 6 commits into from
Jan 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/12433.txt
Original file line number Diff line number Diff line change
@@ -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`.
```
309 changes: 203 additions & 106 deletions internal/service/opsworks/custom_layer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
361 changes: 298 additions & 63 deletions internal/service/opsworks/layers.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 23 additions & 2 deletions website/docs/r/opsworks_custom_layer.html.markdown
Original file line number Diff line number Diff line change
@@ -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: