2016-03-15 16:11:28 +01:00
|
|
|
package test
|
|
|
|
|
|
|
|
import (
|
2016-12-16 15:39:13 +01:00
|
|
|
"reflect"
|
2019-02-11 21:35:46 +01:00
|
|
|
"regexp"
|
2016-03-15 16:11:28 +01:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2018-10-02 01:11:50 +02:00
|
|
|
"github.com/hashicorp/terraform/addrs"
|
2016-03-15 16:11:28 +01:00
|
|
|
"github.com/hashicorp/terraform/helper/resource"
|
2019-03-14 00:17:38 +01:00
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
2016-03-15 16:11:28 +01:00
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestResource_basic(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-03-15 16:11:28 +01:00
|
|
|
}
|
|
|
|
`),
|
2019-01-24 01:17:10 +01:00
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckNoResourceAttr(
|
|
|
|
"test_resource.foo", "list.#",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_changedList(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckNoResourceAttr(
|
|
|
|
"test_resource.foo", "list.#",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
list = ["a"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.#", "1",
|
|
|
|
),
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.0", "a",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
list = ["a", "b"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.#", "2",
|
|
|
|
),
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.0", "a",
|
|
|
|
),
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.1", "b",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
list = ["b"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.#", "1",
|
|
|
|
),
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "list.0", "b",
|
|
|
|
),
|
|
|
|
),
|
2016-03-15 16:11:28 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-03-15 16:03:01 +01:00
|
|
|
// Targeted test in TestContext2Apply_ignoreChangesCreate
|
|
|
|
func TestResource_ignoreChangesRequired(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
2016-06-05 11:04:08 +02:00
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["required"]
|
|
|
|
}
|
2016-03-15 16:03:01 +01:00
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_ignoreChangesEmpty(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
2018-10-16 03:17:27 +02:00
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-03-15 16:03:01 +01:00
|
|
|
optional_force_new = "one"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
2018-10-16 03:17:27 +02:00
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
2016-06-05 11:04:08 +02:00
|
|
|
key = "value"
|
|
|
|
}
|
2016-03-15 16:03:01 +01:00
|
|
|
optional_force_new = "two"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_ignoreChangesForceNew(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2018-10-16 03:17:27 +02:00
|
|
|
required_map = {
|
2016-06-05 11:04:08 +02:00
|
|
|
key = "value"
|
|
|
|
}
|
2016-03-15 16:03:01 +01:00
|
|
|
optional_force_new = "one"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-03-15 16:03:01 +01:00
|
|
|
optional_force_new = "two"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
helper/schema: Normalize bools to "true"/"false" in diffs
For a long time now, the diff logic has relied on the behavior of
`mapstructure.WeakDecode` to determine how various primitives are
converted into strings. The `schema.DiffString` function is used for
all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString.
The `mapstructure` library's string representation of booleans is "0"
and "1", which differs from `strconv.FormatBool`'s "false" and "true"
(which is used in writing out boolean fields to the state).
Because of this difference, diffs have long had the potential for
cosmetically odd but semantically neutral output like:
"true" => "1"
"false" => "0"
So long as `mapstructure.Decode` or `strconv.ParseBool` are used to
interpret these strings, there's no functional problem.
We had our first clear functional problem with #6005 and friends, where
users noticed diffs like the above showing up unexpectedly and causing
troubles when `ignore_changes` was in play.
This particular bug occurs down in Terraform core's EvalIgnoreChanges.
There, the diff is modified to account for ignored attributes, and
special logic attempts to handle properly the situation where the
ignored attribute was going to trigger a resource replacement. That
logic relies on the string representations of the Old and New fields in
the diff to be the same so that it filters properly.
So therefore, we now get a bug when a diff includes `Old: "0", New:
"false"` since the strings do not match, and `ignore_changes` is not
properly handled.
Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`.
I spiked out a full `diffBool` function, but figuring out which pieces
of `diffString` to duplicate there got hairy. This seemed like a simpler
and more direct solution.
Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
|
|
|
// Covers specific scenario in #6005, handled by normalizing boolean strings in
|
|
|
|
// helper/schema
|
|
|
|
func TestResource_ignoreChangesForceNewBoolean(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
helper/schema: Normalize bools to "true"/"false" in diffs
For a long time now, the diff logic has relied on the behavior of
`mapstructure.WeakDecode` to determine how various primitives are
converted into strings. The `schema.DiffString` function is used for
all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString.
The `mapstructure` library's string representation of booleans is "0"
and "1", which differs from `strconv.FormatBool`'s "false" and "true"
(which is used in writing out boolean fields to the state).
Because of this difference, diffs have long had the potential for
cosmetically odd but semantically neutral output like:
"true" => "1"
"false" => "0"
So long as `mapstructure.Decode` or `strconv.ParseBool` are used to
interpret these strings, there's no functional problem.
We had our first clear functional problem with #6005 and friends, where
users noticed diffs like the above showing up unexpectedly and causing
troubles when `ignore_changes` was in play.
This particular bug occurs down in Terraform core's EvalIgnoreChanges.
There, the diff is modified to account for ignored attributes, and
special logic attempts to handle properly the situation where the
ignored attribute was going to trigger a resource replacement. That
logic relies on the string representations of the Old and New fields in
the diff to be the same so that it filters properly.
So therefore, we now get a bug when a diff includes `Old: "0", New:
"false"` since the strings do not match, and `ignore_changes` is not
properly handled.
Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`.
I spiked out a full `diffBool` function, but figuring out which pieces
of `diffString` to duplicate there got hairy. This seemed like a simpler
and more direct solution.
Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
|
|
|
optional_force_new = "one"
|
|
|
|
optional_bool = true
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
helper/schema: Normalize bools to "true"/"false" in diffs
For a long time now, the diff logic has relied on the behavior of
`mapstructure.WeakDecode` to determine how various primitives are
converted into strings. The `schema.DiffString` function is used for
all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString.
The `mapstructure` library's string representation of booleans is "0"
and "1", which differs from `strconv.FormatBool`'s "false" and "true"
(which is used in writing out boolean fields to the state).
Because of this difference, diffs have long had the potential for
cosmetically odd but semantically neutral output like:
"true" => "1"
"false" => "0"
So long as `mapstructure.Decode` or `strconv.ParseBool` are used to
interpret these strings, there's no functional problem.
We had our first clear functional problem with #6005 and friends, where
users noticed diffs like the above showing up unexpectedly and causing
troubles when `ignore_changes` was in play.
This particular bug occurs down in Terraform core's EvalIgnoreChanges.
There, the diff is modified to account for ignored attributes, and
special logic attempts to handle properly the situation where the
ignored attribute was going to trigger a resource replacement. That
logic relies on the string representations of the Old and New fields in
the diff to be the same so that it filters properly.
So therefore, we now get a bug when a diff includes `Old: "0", New:
"false"` since the strings do not match, and `ignore_changes` is not
properly handled.
Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`.
I spiked out a full `diffBool` function, but figuring out which pieces
of `diffString` to duplicate there got hairy. This seemed like a simpler
and more direct solution.
Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
|
|
|
optional_force_new = "two"
|
|
|
|
optional_bool = true
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
2016-07-01 01:22:20 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-03-15 16:03:01 +01:00
|
|
|
func TestResource_ignoreChangesMap(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2018-10-16 03:17:27 +02:00
|
|
|
optional_computed_map = {
|
2016-03-15 16:03:01 +01:00
|
|
|
foo = "bar"
|
|
|
|
}
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_computed_map"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
2016-06-05 11:04:08 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2018-10-16 03:17:27 +02:00
|
|
|
optional_computed_map = {
|
2016-03-15 16:03:01 +01:00
|
|
|
foo = "bar"
|
|
|
|
no = "update"
|
|
|
|
}
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_computed_map"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-07-08 16:14:06 +02:00
|
|
|
func TestResource_ignoreChangesDependent(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
count = 2
|
|
|
|
required = "yep"
|
2018-10-16 03:17:27 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-07-08 16:14:06 +02:00
|
|
|
|
|
|
|
optional_force_new = "one"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resource "test_resource" "bar" {
|
|
|
|
count = 2
|
|
|
|
required = "yep"
|
2018-10-16 03:17:27 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-07-08 16:14:06 +02:00
|
|
|
optional = "${element(test_resource.foo.*.id, count.index)}"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
count = 2
|
|
|
|
required = "yep"
|
2018-10-16 03:17:27 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-07-08 16:14:06 +02:00
|
|
|
|
|
|
|
optional_force_new = "two"
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_force_new"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resource "test_resource" "bar" {
|
|
|
|
count = 2
|
|
|
|
required = "yep"
|
2018-10-16 03:17:27 +02:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2016-07-08 16:14:06 +02:00
|
|
|
optional = "${element(test_resource.foo.*.id, count.index)}"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_ignoreChangesStillReplaced(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_force_new = "one"
|
|
|
|
optional_bool = true
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_bool"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_force_new = "two"
|
|
|
|
optional_bool = false
|
|
|
|
lifecycle {
|
|
|
|
ignore_changes = ["optional_bool"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-11-18 21:47:30 +01:00
|
|
|
// Reproduces plan-time panic when the wrong type is interpolated in a list of
|
|
|
|
// maps.
|
|
|
|
// TODO: this should return a type error, rather than silently setting an empty
|
|
|
|
// list
|
|
|
|
func TestResource_dataSourceListMapPanic(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "val"
|
|
|
|
required_map = {x = "y"}
|
|
|
|
list_of_map = "${var.maplist}"
|
|
|
|
}
|
|
|
|
|
|
|
|
variable "maplist" {
|
|
|
|
type = "list"
|
|
|
|
|
|
|
|
default = [
|
|
|
|
{a = "b"}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectError: nil,
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-12-16 15:39:13 +01:00
|
|
|
func TestResource_dataSourceIndexMapList(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "val"
|
|
|
|
|
|
|
|
required_map = {
|
|
|
|
x = "y"
|
|
|
|
}
|
|
|
|
|
|
|
|
list_of_map = [
|
|
|
|
{
|
|
|
|
a = "1"
|
|
|
|
b = "2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
c = "3"
|
|
|
|
d = "4"
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
output "map_from_list" {
|
|
|
|
value = "${test_resource.foo.list_of_map[0]}"
|
|
|
|
}
|
|
|
|
|
|
|
|
output "value_from_map_from_list" {
|
|
|
|
value = "${lookup(test_resource.foo.list_of_map[1], "d")}"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectError: nil,
|
|
|
|
Check: func(s *terraform.State) error {
|
2018-10-02 01:11:50 +02:00
|
|
|
root := s.ModuleByPath(addrs.RootModuleInstance)
|
2016-12-16 15:39:13 +01:00
|
|
|
mapOut := root.Outputs["map_from_list"].Value
|
|
|
|
expectedMapOut := map[string]interface{}{
|
|
|
|
"a": "1",
|
|
|
|
"b": "2",
|
|
|
|
}
|
|
|
|
|
|
|
|
valueOut := root.Outputs["value_from_map_from_list"].Value
|
|
|
|
expectedValueOut := "4"
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(mapOut, expectedMapOut) {
|
|
|
|
t.Fatalf("Expected: %#v\nGot: %#v", expectedMapOut, mapOut)
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(valueOut, expectedValueOut) {
|
|
|
|
t.Fatalf("Expected: %#v\nGot: %#v", valueOut, expectedValueOut)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-03-15 16:11:28 +01:00
|
|
|
func testAccCheckResourceDestroy(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
}
|
2018-10-31 18:42:28 +01:00
|
|
|
|
|
|
|
func TestResource_removeForceNew(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_force_new = "here"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2018-11-27 00:12:59 +01:00
|
|
|
|
|
|
|
func TestResource_unknownFuncInMap(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "ok"
|
|
|
|
required_map = {
|
|
|
|
key = "${uuid()}"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectNonEmptyPlan: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2018-12-12 22:08:28 +01:00
|
|
|
|
|
|
|
// Verify that we can destroy when a managed resource references something with
|
|
|
|
// a count of 1.
|
|
|
|
func TestResource_countRefDestroyError(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "one" {
|
|
|
|
count = 1
|
|
|
|
required = "ok"
|
|
|
|
required_map = {
|
|
|
|
key = "val"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "two" {
|
|
|
|
required = test_resource.one[0].id
|
|
|
|
required_map = {
|
|
|
|
key = "val"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-01-08 20:18:53 +01:00
|
|
|
|
|
|
|
func TestResource_emptyMapValue(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "ok"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
b = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-02-11 21:35:46 +01:00
|
|
|
|
|
|
|
func TestResource_updateError(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "first"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
apply_error = "update_error"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectError: regexp.MustCompile("update_error"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_applyError(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
apply_error = "apply_error"
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectError: regexp.MustCompile("apply_error"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-02-12 01:24:14 +01:00
|
|
|
|
|
|
|
func TestResource_emptyStrings(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
|
|
|
|
list = [""]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr("test_resource.foo", "list.0", ""),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
|
|
|
|
list = ["", "b"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr("test_resource.foo", "list.0", ""),
|
|
|
|
resource.TestCheckResourceAttr("test_resource.foo", "list.1", "b"),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
|
|
|
|
list = [""]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr("test_resource.foo", "list.0", ""),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-03-14 00:17:38 +01:00
|
|
|
|
|
|
|
func TestResource_setDrift(t *testing.T) {
|
|
|
|
testProvider := testAccProviders["test"]
|
|
|
|
res := testProvider.(*schema.Provider).ResourcesMap["test_resource"]
|
|
|
|
|
|
|
|
// reset the Read function after the test
|
|
|
|
defer func() {
|
|
|
|
res.Read = testResourceRead
|
|
|
|
}()
|
|
|
|
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "first"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
set = ["a", "b"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: func(s *terraform.State) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
PreConfig: func() {
|
|
|
|
// update the Read function to return the wrong "set" attribute values.
|
|
|
|
res.Read = func(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
// update as expected first
|
|
|
|
if err := testResourceRead(d, meta); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
d.Set("set", []interface{}{"a", "x"})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Leave the config, so we can detect the mismatched set values.
|
|
|
|
// Updating the config would force the test to pass even if the Read
|
|
|
|
// function values were ignored.
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "second"
|
|
|
|
required_map = {
|
|
|
|
a = "a"
|
|
|
|
}
|
|
|
|
set = ["a", "b"]
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
ExpectNonEmptyPlan: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-03-28 21:21:58 +01:00
|
|
|
|
|
|
|
func TestResource_optionalComputedMap(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_computed_map = {
|
|
|
|
foo = "bar"
|
|
|
|
baz = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "optional_computed_map.foo", "bar",
|
|
|
|
),
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "optional_computed_map.baz", "",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_computed_map = {}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
// removing the map from the config should still leave an empty computed map
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "optional_computed_map.%", "0",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestResource_plannedComputed(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
2019-04-03 23:36:08 +02:00
|
|
|
required = "ok"
|
2019-03-28 21:21:58 +01:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2019-04-03 23:36:08 +02:00
|
|
|
optional = "hi"
|
2019-03-28 21:21:58 +01:00
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
2019-04-03 23:36:08 +02:00
|
|
|
"test_resource.foo", "planned_computed", "hi",
|
2019-03-28 21:21:58 +01:00
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
2019-04-03 23:36:08 +02:00
|
|
|
required = "ok"
|
2019-03-28 21:21:58 +01:00
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
2019-04-03 23:36:08 +02:00
|
|
|
optional = "changed"
|
2019-03-28 21:21:58 +01:00
|
|
|
}
|
|
|
|
`),
|
|
|
|
Check: resource.ComposeTestCheckFunc(
|
|
|
|
resource.TestCheckResourceAttr(
|
|
|
|
"test_resource.foo", "planned_computed", "changed",
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestDiffApply_map(t *testing.T) {
|
|
|
|
resSchema := map[string]*schema.Schema{
|
|
|
|
"map": {
|
|
|
|
Type: schema.TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
priorAttrs := map[string]string{
|
|
|
|
"id": "ok",
|
|
|
|
"map.%": "2",
|
|
|
|
"map.foo": "bar",
|
|
|
|
"map.bar": "",
|
|
|
|
}
|
|
|
|
|
|
|
|
diff := &terraform.InstanceDiff{
|
|
|
|
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
|
|
"map.foo": &terraform.ResourceAttrDiff{Old: "bar", New: "", NewRemoved: true},
|
|
|
|
"map.bar": &terraform.ResourceAttrDiff{Old: "", New: "", NewRemoved: true},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
newAttrs, err := diff.Apply(priorAttrs, (&schema.Resource{Schema: resSchema}).CoreConfigSchema())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
expect := map[string]string{
|
|
|
|
"id": "ok",
|
|
|
|
"map.%": "0",
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(newAttrs, expect) {
|
|
|
|
t.Fatalf("expected:%#v got:%#v", expect, newAttrs)
|
|
|
|
}
|
|
|
|
}
|
2019-04-03 23:36:08 +02:00
|
|
|
|
|
|
|
func TestResource_dependsComputed(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
variable "change" {
|
|
|
|
default = false
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "ok"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional = var.change ? "after" : ""
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "bar" {
|
|
|
|
count = var.change ? 1 : 0
|
|
|
|
required = test_resource.foo.planned_computed
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
variable "change" {
|
|
|
|
default = true
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "ok"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional = var.change ? "after" : ""
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "bar" {
|
|
|
|
count = var.change ? 1 : 0
|
|
|
|
required = test_resource.foo.planned_computed
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-04-22 22:41:26 +02:00
|
|
|
|
|
|
|
func TestResource_optionalComputedBool(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2019-05-07 01:19:10 +02:00
|
|
|
|
|
|
|
func TestResource_replacedOptionalComputed(t *testing.T) {
|
|
|
|
resource.UnitTest(t, resource.TestCase{
|
|
|
|
Providers: testAccProviders,
|
|
|
|
CheckDestroy: testAccCheckResourceDestroy,
|
|
|
|
Steps: []resource.TestStep{
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource_nested" "a" {
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_computed = test_resource_nested.a.id
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
resource.TestStep{
|
|
|
|
Config: strings.TrimSpace(`
|
|
|
|
resource "test_resource_nested" "b" {
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_resource" "foo" {
|
|
|
|
required = "yep"
|
|
|
|
required_map = {
|
|
|
|
key = "value"
|
|
|
|
}
|
|
|
|
optional_computed = test_resource_nested.b.id
|
|
|
|
}
|
|
|
|
`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|