From 24c3dc038654c5134245034d54e998747f7c9658 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 18:24:50 -0500 Subject: [PATCH 01/19] trying something different --- docker/label.go | 53 +++++++++++++++++++++++++++ docker/resource_docker_secret.go | 7 ++-- docker/resource_docker_secret_test.go | 18 ++++++--- 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 docker/label.go diff --git a/docker/label.go b/docker/label.go new file mode 100644 index 00000000..32b60ec3 --- /dev/null +++ b/docker/label.go @@ -0,0 +1,53 @@ +package docker + +import "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + +func labelToPair(label map[string]interface{}) (string, string) { + return label["label"].(string), label["value"].(string) +} + +func labelSetToMap(labels *schema.Set) map[string]string { + labelsSlice := labels.List() + + mapped := make(map[string]string, len(labelsSlice)) + for _, label := range labelsSlice { + l, v := labelToPair(label.(map[string]interface{})) + mapped[l] = v + } + return mapped +} + +func hashLabel(v interface{}) int { + labelMap := v.(map[string]interface{}) + return hashStringLabel(labelMap["label"].(string)) +} + +func hashStringLabel(str string) int { + return schema.HashString(str) +} + +func mapToLabelSet(labels map[string]string) *schema.Set { + var mapped []interface{} + for k, v := range labels { + mapped = append(mapped, map[string]interface{}{ + "label": k, + "value": v, + }) + } + return schema.NewSet(hashLabel, mapped) +} + +var labelSchema = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "label": &schema.Schema{ + Type: schema.TypeString, + Description: "Name of the label", + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Description: "Value of the label", + Required: true, + }, + }, +} diff --git a/docker/resource_docker_secret.go b/docker/resource_docker_secret.go index 6c15f934..b5bf8de9 100644 --- a/docker/resource_docker_secret.go +++ b/docker/resource_docker_secret.go @@ -5,6 +5,7 @@ import ( "log" "context" + "github.com/docker/docker/api/types/swarm" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) @@ -33,9 +34,9 @@ func resourceDockerSecret() *schema.Resource { }, "labels": { - Type: schema.TypeMap, - Optional: true, + Type: schema.TypeSet, ForceNew: true, + Elem: labelSchema, }, }, } @@ -53,7 +54,7 @@ func resourceDockerSecretCreate(d *schema.ResourceData, meta interface{}) error } if v, ok := d.GetOk("labels"); ok { - secretSpec.Annotations.Labels = mapTypeMapValsToString(v.(map[string]interface{})) + secretSpec.Annotations.Labels = labelSetToMap(v.(*schema.Set)) } secret, err := client.SecretCreate(context.Background(), secretSpec) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 5df18391..2af51b84 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -5,6 +5,7 @@ import ( "testing" "context" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" ) @@ -74,6 +75,7 @@ func TestAccDockerSecret_basicUpdatable(t *testing.T) { } func TestAccDockerSecret_labels(t *testing.T) { + var test1LabelKey = hashStringLabel("test1") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -84,15 +86,21 @@ func TestAccDockerSecret_labels(t *testing.T) { resource "docker_secret" "foo" { name = "foo-secret" data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" - labels = { - "test1" = "foo" - "test2" = "bar" + labels { + label = "test1" + value = "foo" + } + labels { + label = "test2" + value = "bar" } } `, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("docker_secret.foo", "labels.test1", "foo"), - resource.TestCheckResourceAttr("docker_secret.foo", "labels.test2", "bar"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.label", hashStringLabel("test1")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.value", hashStringLabel("test1")), "foo"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.label", hashStringLabel("test2")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.value", hashStringLabel("test2")), "bar"), ), }, }, From 2ddb6c9308145302fa0c93dab434349696c3b1e7 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 18:33:52 -0500 Subject: [PATCH 02/19] woops --- docker/resource_docker_secret_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 2af51b84..3e10271c 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -75,7 +75,6 @@ func TestAccDockerSecret_basicUpdatable(t *testing.T) { } func TestAccDockerSecret_labels(t *testing.T) { - var test1LabelKey = hashStringLabel("test1") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, From 1aa89c5e2183037b8d322cf230a718467795fda1 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 18:44:08 -0500 Subject: [PATCH 03/19] use %v not %s --- docker/resource_docker_secret_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 3e10271c..4cb026bd 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -96,10 +96,10 @@ func TestAccDockerSecret_labels(t *testing.T) { } `, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.label", hashStringLabel("test1")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.value", hashStringLabel("test1")), "foo"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.label", hashStringLabel("test2")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%s.value", hashStringLabel("test2")), "bar"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test1")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test1")), "foo"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test2")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test2")), "bar"), ), }, }, From 7f548cd95a50c1db12f3ad80a0451679eee42ece Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 18:56:55 -0500 Subject: [PATCH 04/19] specify labels are optional --- docker/resource_docker_secret.go | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/resource_docker_secret.go b/docker/resource_docker_secret.go index b5bf8de9..14e4960a 100644 --- a/docker/resource_docker_secret.go +++ b/docker/resource_docker_secret.go @@ -35,6 +35,7 @@ func resourceDockerSecret() *schema.Resource { "labels": { Type: schema.TypeSet, + Optional: true, ForceNew: true, Elem: labelSchema, }, From 95211f04f72e9909b32fd6cd9841861d46f72aa1 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 19:13:31 -0500 Subject: [PATCH 05/19] try straight indices --- docker/resource_docker_secret_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 4cb026bd..e1c7c855 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -96,10 +96,10 @@ func TestAccDockerSecret_labels(t *testing.T) { } `, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test1")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test1")), "foo"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test2")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test2")), "bar"), + resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.label", "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.value", "foo"), + resource.TestCheckResourceAttr("docker_secret.foo", "labels.1.label", "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", "labels.1.value", "bar"), ), }, }, From 7e84878d39b342e4de0539c6bb0c2e35fdacd18d Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 23:15:34 -0500 Subject: [PATCH 06/19] get a look at all the Attributes --- docker/resource_docker_secret_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index e1c7c855..b4fbe7b1 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -6,6 +6,7 @@ import ( "context" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" ) @@ -96,6 +97,10 @@ func TestAccDockerSecret_labels(t *testing.T) { } `, Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + spew.Dump(s.RootModule().Resources["docker_secret.foo"].Primary.Attributes) + return nil + }, resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.label", "test1"), resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.value", "foo"), resource.TestCheckResourceAttr("docker_secret.foo", "labels.1.label", "test1"), From b9dd25a94d95daf6ba700664ef0cfded34a9af37 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 8 Nov 2019 23:29:23 -0500 Subject: [PATCH 07/19] try this again? --- docker/resource_docker_secret_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index b4fbe7b1..5ee2a378 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -101,10 +101,10 @@ func TestAccDockerSecret_labels(t *testing.T) { spew.Dump(s.RootModule().Resources["docker_secret.foo"].Primary.Attributes) return nil }, - resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.label", "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", "labels.0.value", "foo"), - resource.TestCheckResourceAttr("docker_secret.foo", "labels.1.label", "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", "labels.1.value", "bar"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test1")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test1")), "foo"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test2")), "test1"), + resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test2")), "bar"), ), }, }, From 0c39299bad3ce11336b8174f22c20b2c7462d6b8 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Sat, 9 Nov 2019 00:03:19 -0500 Subject: [PATCH 08/19] try this! --- docker/label.go | 35 ++++++++++++++++++++++++++- docker/resource_docker_secret_test.go | 15 +++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docker/label.go b/docker/label.go index 32b60ec3..2a7ad6fb 100644 --- a/docker/label.go +++ b/docker/label.go @@ -1,6 +1,10 @@ package docker -import "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +import ( + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) func labelToPair(label map[string]interface{}) (string, string) { return label["label"].(string), label["value"].(string) @@ -51,3 +55,32 @@ var labelSchema = &schema.Resource{ }, }, } + +//gatherImmediateSubkeys given an incomplete attribute identifier, find all +//the strings (if any) that appear after this one in the various dot-separated +//identifiers. +func gatherImmediateSubkeys(attrs map[string]string, partialKey string) []string { + var immediateSubkeys = []string{} + for k := range attrs { + prefix := partialKey + "." + if strings.HasPrefix(k, prefix) { + rest := strings.TrimPrefix(k, prefix) + parts := strings.SplitN(rest, ".", 2) + immediateSubkeys = append(immediateSubkeys, parts[0]) + } + } + + return immediateSubkeys +} + +func getLabelMapForPartialKey(attrs map[string]string, partialKey string) map[string]string { + setIDs := gatherImmediateSubkeys(attrs, partialKey) + + var labelMap = map[string]string{} + for _, id := range setIDs { + prefix := partialKey + "." + id + labelMap[attrs[prefix+".label"]] = attrs[prefix+".value"] + } + + return labelMap +} diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 5ee2a378..810c6ba9 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -6,7 +6,6 @@ import ( "context" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" ) @@ -98,13 +97,17 @@ func TestAccDockerSecret_labels(t *testing.T) { `, Check: resource.ComposeTestCheckFunc( func(s *terraform.State) error { - spew.Dump(s.RootModule().Resources["docker_secret.foo"].Primary.Attributes) + attrs := s.RootModule().Resources["docker_secret.foo"].Primary.Attributes + labelMap := getLabelMapForPartialKey(attrs, "labels") + + if len(labelMap) != 2 || + labelMap["test1"] != "foo" || + labelMap["test2"] != "bar" { + return fmt.Errorf("label map had unexpected structure: %v", labelMap) + } + return nil }, - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test1")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test1")), "foo"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.label", hashStringLabel("test2")), "test1"), - resource.TestCheckResourceAttr("docker_secret.foo", fmt.Sprintf("labels.%v.value", hashStringLabel("test2")), "bar"), ), }, }, From cfb13bb53218aeee44297e47bdac781c6ae35277 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Sat, 9 Nov 2019 07:03:15 -0500 Subject: [PATCH 09/19] skip the size var --- docker/label.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/label.go b/docker/label.go index 2a7ad6fb..a5888cd0 100644 --- a/docker/label.go +++ b/docker/label.go @@ -78,6 +78,9 @@ func getLabelMapForPartialKey(attrs map[string]string, partialKey string) map[st var labelMap = map[string]string{} for _, id := range setIDs { + if id == "#" { + continue + } prefix := partialKey + "." + id labelMap[attrs[prefix+".label"]] = attrs[prefix+".value"] } From e3912304470df6e997f2925c703cb8c05b9bd2c1 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Sun, 10 Nov 2019 15:15:42 -0500 Subject: [PATCH 10/19] migrate the container resource --- docker/resource_docker_container.go | 7 ++++--- docker/resource_docker_container_funcs.go | 4 ++-- docker/resource_docker_container_test.go | 10 +++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 176541ec..91215abb 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -256,10 +256,10 @@ func resourceDockerContainer() *schema.Resource { Optional: true, }, "labels": { - Type: schema.TypeMap, + Type: schema.TypeSet, Description: "User-defined key/value metadata", Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: labelSchema, }, "driver_name": { Type: schema.TypeString, @@ -536,9 +536,10 @@ func resourceDockerContainer() *schema.Resource { }, "labels": { - Type: schema.TypeMap, + Type: schema.TypeSet, Optional: true, ForceNew: true, + Elem: labelSchema, }, "memory": { diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index c078e9e3..a80adfea 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -104,7 +104,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } if v, ok := d.GetOk("labels"); ok { - config.Labels = mapTypeMapValsToString(v.(map[string]interface{})) + config.Labels = labelSetToMap(v.(*schema.Set)) } if value, ok := d.GetOk("healthcheck"); ok { @@ -168,7 +168,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err mountInstance.VolumeOptions.NoCopy = value.(bool) } if value, ok := rawVolumeOptions["labels"]; ok { - mountInstance.VolumeOptions.Labels = mapTypeMapValsToString(value.(map[string]interface{})) + mountInstance.VolumeOptions.Labels = labelSetToMap(value.(*schema.Set)) } // because it is not possible to nest maps if value, ok := rawVolumeOptions["driver_name"]; ok { diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index ed21ae5f..587fab29 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -1660,9 +1660,13 @@ resource "docker_container" "foo" { dns = ["8.8.8.8"] dns_opts = ["rotate"] dns_search = ["example.com"] - labels = { - env = "prod" - role = "test" + labels { + label = "env" + value = "prod" + } + labels { + label = "role" + value = "test" } log_driver = "json-file" log_opts = { From 5babf5fb171f4de0bda1bb5b304641d79f138107 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Tue, 12 Nov 2019 17:41:59 -0500 Subject: [PATCH 11/19] secret and network cleanup --- docker/label.go | 38 ++++++++ docker/resource_docker_network.go | 124 ++++++++++++++++++++++++ docker/resource_docker_network_funcs.go | 2 +- docker/resource_docker_network_test.go | 19 +++- docker/resource_docker_secret.go | 43 ++++++++ docker/resource_docker_secret_test.go | 16 +-- 6 files changed, 224 insertions(+), 18 deletions(-) diff --git a/docker/label.go b/docker/label.go index a5888cd0..b7213ba9 100644 --- a/docker/label.go +++ b/docker/label.go @@ -1,9 +1,12 @@ package docker import ( + "fmt" "strings" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" ) func labelToPair(label map[string]interface{}) (string, string) { @@ -56,6 +59,22 @@ var labelSchema = &schema.Resource{ }, } +//unfortunately, _one_ of the several place that the old label schema was +//used specified that the keys had to be strings, while the others allowed +//any type of key and coerced them into strings. +func upgradeLabelMapFromV0ToV1(labelMap map[string]interface{}) []map[string]string { + var migratedState []map[string]string + + for l, v := range labelMap { + migratedState = append(migratedState, map[string]string{ + "label": l, + "value": fmt.Sprintf("%v", v), + }) + } + + return migratedState +} + //gatherImmediateSubkeys given an incomplete attribute identifier, find all //the strings (if any) that appear after this one in the various dot-separated //identifiers. @@ -87,3 +106,22 @@ func getLabelMapForPartialKey(attrs map[string]string, partialKey string) map[st return labelMap } + +func testCheckLabelMap(name string, partialKey string, expectedLabels map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + attrs := s.RootModule().Resources[name].Primary.Attributes + labelMap := getLabelMapForPartialKey(attrs, partialKey) + + if len(labelMap) != len(expectedLabels) { + return fmt.Errorf("expected %v labels, found %v", len(expectedLabels), len(labelMap)) + } + + for l, v := range expectedLabels { + if labelMap[l] != v { + return fmt.Errorf("expected value %v for label %v, got %v", v, l, labelMap[v]) + } + } + + return nil + } +} diff --git a/docker/resource_docker_network.go b/docker/resource_docker_network.go index aaf6a99f..9d4ebba3 100644 --- a/docker/resource_docker_network.go +++ b/docker/resource_docker_network.go @@ -15,6 +15,130 @@ func resourceDockerNetwork() *schema.Resource { Read: resourceDockerNetworkRead, Delete: resourceDockerNetworkDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "labels": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: labelSchema, + }, + + "check_duplicate": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "driver": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "options": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "internal": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "attachable": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "ingress": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "ipv6": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "ipam_driver": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ipam_config": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ip_range": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "gateway": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "aux_address": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + }, + Set: resourceDockerIpamConfigHash, + }, + + "scope": { + Type: schema.TypeString, + Computed: true, + }, + }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceDockerNetworkV0().CoreConfigSchema().ImpliedType(), + Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + labelMap := rawState["labels"].(map[string]interface{}) + rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + + return rawState, nil + }, + }, + }, + } +} + +func resourceDockerNetworkV0() *schema.Resource { + return &schema.Resource{ + //This is only used for state migration, so the CRUD + //callbacks are no longer relevant Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, diff --git a/docker/resource_docker_network_funcs.go b/docker/resource_docker_network_funcs.go index dac6acb2..48ddbbd5 100644 --- a/docker/resource_docker_network_funcs.go +++ b/docker/resource_docker_network_funcs.go @@ -20,7 +20,7 @@ func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error createOpts := types.NetworkCreate{} if v, ok := d.GetOk("labels"); ok { - createOpts.Labels = mapTypeMapValsToString(v.(map[string]interface{})) + createOpts.Labels = labelSetToMap(v.(*schema.Set)) } if v, ok := d.GetOk("check_duplicate"); ok { createOpts.CheckDuplicate = v.(bool) diff --git a/docker/resource_docker_network_test.go b/docker/resource_docker_network_test.go index 058aacf3..354a0b80 100644 --- a/docker/resource_docker_network_test.go +++ b/docker/resource_docker_network_test.go @@ -5,6 +5,7 @@ import ( "testing" "context" + "github.com/docker/docker/api/types" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" @@ -260,8 +261,12 @@ func TestAccDockerNetwork_labels(t *testing.T) { Config: testAccDockerNetworkLabelsConfig, Check: resource.ComposeTestCheckFunc( testAccNetwork("docker_network.foo", &n), - testAccNetworkLabel(&n, "com.docker.compose.network", "foo"), - testAccNetworkLabel(&n, "com.docker.compose.project", "test"), + testCheckLabelMap("docker_network.foo", "labels", + map[string]string{ + "com.docker.compose.network": "foo", + "com.docker.compose.project": "test", + }, + ), ), }, }, @@ -280,9 +285,13 @@ func testAccNetworkLabel(network *types.NetworkResource, name string, value stri const testAccDockerNetworkLabelsConfig = ` resource "docker_network" "foo" { name = "test_foo" - labels = { - "com.docker.compose.network" = "foo" - "com.docker.compose.project" = "test" + labels { + label = "com.docker.compose.network" + value = "foo" + } + labels { + label = "com.docker.compose.project" + value = "test" } } ` diff --git a/docker/resource_docker_secret.go b/docker/resource_docker_secret.go index 14e4960a..35c8f6bb 100644 --- a/docker/resource_docker_secret.go +++ b/docker/resource_docker_secret.go @@ -40,6 +40,49 @@ func resourceDockerSecret() *schema.Resource { Elem: labelSchema, }, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceDockerSecretV0().CoreConfigSchema().ImpliedType(), + Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + labelMap := rawState["labels"].(map[string]interface{}) + rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + + return rawState, nil + }, + }, + }, + } +} + +func resourceDockerSecretV0() *schema.Resource { + return &schema.Resource{ + //This is only used for state migration, so the CRUD + //callbacks are no longer relevant + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "User-defined name of the secret", + Required: true, + ForceNew: true, + }, + + "data": { + Type: schema.TypeString, + Description: "User-defined name of the secret", + Required: true, + Sensitive: true, + ForceNew: true, + ValidateFunc: validateStringIsBase64Encoded(), + }, + + "labels": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, } } diff --git a/docker/resource_docker_secret_test.go b/docker/resource_docker_secret_test.go index 810c6ba9..f6a089c7 100644 --- a/docker/resource_docker_secret_test.go +++ b/docker/resource_docker_secret_test.go @@ -95,18 +95,10 @@ func TestAccDockerSecret_labels(t *testing.T) { } } `, - Check: resource.ComposeTestCheckFunc( - func(s *terraform.State) error { - attrs := s.RootModule().Resources["docker_secret.foo"].Primary.Attributes - labelMap := getLabelMapForPartialKey(attrs, "labels") - - if len(labelMap) != 2 || - labelMap["test1"] != "foo" || - labelMap["test2"] != "bar" { - return fmt.Errorf("label map had unexpected structure: %v", labelMap) - } - - return nil + Check: testCheckLabelMap("docker_secret.foo", "labels", + map[string]string{ + "test1": "foo", + "test2": "bar", }, ), }, From 32067443cac78064ea0617a766e788926d528f1e Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Thu, 14 Nov 2019 07:58:53 -0500 Subject: [PATCH 12/19] volume --- docker/resource_docker_volume.go | 58 +++++++++++++++++++++++++-- docker/resource_docker_volume_test.go | 19 ++++++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/docker/resource_docker_volume.go b/docker/resource_docker_volume.go index 388ff7f3..a4006edc 100644 --- a/docker/resource_docker_volume.go +++ b/docker/resource_docker_volume.go @@ -3,13 +3,14 @@ package docker import ( "context" "fmt" + "log" + "strings" + "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/volume" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "log" - "strings" - "time" ) func resourceDockerVolume() *schema.Resource { @@ -18,6 +19,55 @@ func resourceDockerVolume() *schema.Resource { Read: resourceDockerVolumeRead, Delete: resourceDockerVolumeDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "labels": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: labelSchema, + }, + "driver": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "driver_opts": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + "mountpoint": { + Type: schema.TypeString, + Computed: true, + }, + }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceDockerVolumeV0().CoreConfigSchema().ImpliedType(), + Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + labelMap := rawState["labels"].(map[string]interface{}) + rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + + return rawState, nil + }, + }, + }, + } +} + +func resourceDockerVolumeV0() *schema.Resource { + return &schema.Resource{ + //This is only used for state migration, so the CRUD + //callbacks are no longer relevant Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -59,7 +109,7 @@ func resourceDockerVolumeCreate(d *schema.ResourceData, meta interface{}) error createOpts.Name = v.(string) } if v, ok := d.GetOk("labels"); ok { - createOpts.Labels = mapTypeMapValsToString(v.(map[string]interface{})) + createOpts.Labels = labelSetToMap(v.(*schema.Set)) } if v, ok := d.GetOk("driver"); ok { createOpts.Driver = v.(string) diff --git a/docker/resource_docker_volume_test.go b/docker/resource_docker_volume_test.go index e0b5f938..5898332c 100644 --- a/docker/resource_docker_volume_test.go +++ b/docker/resource_docker_volume_test.go @@ -3,10 +3,11 @@ package docker import ( "context" "fmt" + "testing" + "github.com/docker/docker/api/types" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" - "testing" ) func TestAccDockerVolume_basic(t *testing.T) { @@ -69,8 +70,12 @@ func TestAccDockerVolume_labels(t *testing.T) { Config: testAccDockerVolumeLabelsConfig, Check: resource.ComposeTestCheckFunc( checkDockerVolume("docker_volume.foo", &v), - testAccVolumeLabel(&v, "com.docker.compose.project", "test"), - testAccVolumeLabel(&v, "com.docker.compose.volume", "foo"), + testCheckLabelMap("docker_volume.foo", "labels", + map[string]string{ + "com.docker.compose.project": "test", + "com.docker.compose.volume": "foo", + }, + ), ), }, }, @@ -90,8 +95,12 @@ const testAccDockerVolumeLabelsConfig = ` resource "docker_volume" "foo" { name = "test_foo" labels = { - "com.docker.compose.project" = "test" - "com.docker.compose.volume" = "foo" + label = "com.docker.compose.project" + value = "test" + } + labels = { + label = "com.docker.compose.volume" + value = "foo" } } ` From 080d80702aceb66c76f2e5b3d94b0ec3eca69bfe Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Thu, 14 Nov 2019 08:06:50 -0500 Subject: [PATCH 13/19] service --- docker/resource_docker_service.go | 888 ++++++++++++++++++++++++ docker/resource_docker_service_funcs.go | 8 +- docker/resource_docker_service_test.go | 55 +- 3 files changed, 930 insertions(+), 21 deletions(-) diff --git a/docker/resource_docker_service.go b/docker/resource_docker_service.go index 813f1c44..038ed32e 100644 --- a/docker/resource_docker_service.go +++ b/docker/resource_docker_service.go @@ -18,6 +18,894 @@ func resourceDockerService() *schema.Resource { Delete: resourceDockerServiceDelete, Exists: resourceDockerServiceExists, + Schema: map[string]*schema.Schema{ + "auth": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server_address": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "username": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""), + }, + "password": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""), + Sensitive: true, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Description: "Name of the service", + Required: true, + ForceNew: true, + }, + "labels": { + Type: schema.TypeSet, + Description: "User-defined key/value metadata", + Optional: true, + Computed: true, + Elem: labelSchema, + }, + "task_spec": { + Type: schema.TypeList, + Description: "User modifiable task configuration", + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "container_spec": { + Type: schema.TypeList, + Description: "The spec for each container", + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "image": { + Type: schema.TypeString, + Description: "The image name to use for the containers of the service", + Required: true, + DiffSuppressFunc: suppressIfSHAwasAdded(), + }, + "labels": { + Type: schema.TypeSet, + Description: "User-defined key/value metadata", + Optional: true, + Elem: labelSchema, + }, + "command": { + Type: schema.TypeList, + Description: "The command to be run in the image", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "args": { + Type: schema.TypeList, + Description: "Arguments to the command", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "hostname": { + Type: schema.TypeString, + Description: "The hostname to use for the container, as a valid RFC 1123 hostname", + Optional: true, + }, + "env": { + Type: schema.TypeMap, + Description: "A list of environment variables in the form VAR=\"value\"", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dir": { + Type: schema.TypeString, + Description: "The working directory for commands to run in", + Optional: true, + }, + "user": { + Type: schema.TypeString, + Description: "The user inside the container", + Optional: true, + }, + "groups": { + Type: schema.TypeList, + Description: "A list of additional groups that the container process will run as", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "privileges": { + Type: schema.TypeList, + Description: "Security options for the container", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "credential_spec": { + Type: schema.TypeList, + Description: "CredentialSpec for managed service account (Windows only)", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "file": { + Type: schema.TypeString, + Description: "Load credential spec from this file", + Optional: true, + }, + "registry": { + Type: schema.TypeString, + Description: "Load credential spec from this value in the Windows registry", + Optional: true, + }, + }, + }, + }, + "se_linux_context": { + Type: schema.TypeList, + Description: "SELinux labels of the container", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "disable": { + Type: schema.TypeBool, + Description: "Disable SELinux", + Optional: true, + }, + "user": { + Type: schema.TypeString, + Description: "SELinux user label", + Optional: true, + }, + "role": { + Type: schema.TypeString, + Description: "SELinux role label", + Optional: true, + }, + "type": { + Type: schema.TypeString, + Description: "SELinux type label", + Optional: true, + }, + "level": { + Type: schema.TypeString, + Description: "SELinux level label", + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "read_only": { + Type: schema.TypeBool, + Description: "Mount the container's root filesystem as read only", + Optional: true, + }, + "mounts": { + Type: schema.TypeSet, + Description: "Specification for mounts to be added to containers created as part of the service", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target": { + Type: schema.TypeString, + Description: "Container path", + Required: true, + }, + "source": { + Type: schema.TypeString, + Description: "Mount source (e.g. a volume name, a host path)", + Optional: true, + }, + "type": { + Type: schema.TypeString, + Description: "The mount type", + Required: true, + ValidateFunc: validateStringMatchesPattern(`^(bind|volume|tmpfs)$`), + }, + "read_only": { + Type: schema.TypeBool, + Description: "Whether the mount should be read-only", + Optional: true, + }, + "bind_options": { + Type: schema.TypeList, + Description: "Optional configuration for the bind type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "propagation": { + Type: schema.TypeString, + Description: "A propagation mode with the value", + Optional: true, + ValidateFunc: validateStringMatchesPattern(`^(private|rprivate|shared|rshared|slave|rslave)$`), + }, + }, + }, + }, + "volume_options": { + Type: schema.TypeList, + Description: "Optional configuration for the volume type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "no_copy": { + Type: schema.TypeBool, + Description: "Populate volume with data from the target", + Optional: true, + }, + "labels": { + Type: schema.TypeSet, + Description: "User-defined key/value metadata", + Optional: true, + Elem: labelSchema, + }, + "driver_name": { + Type: schema.TypeString, + Description: "Name of the driver to use to create the volume.", + Optional: true, + }, + "driver_options": { + Type: schema.TypeMap, + Description: "key/value map of driver specific options", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tmpfs_options": { + Type: schema.TypeList, + Description: "Optional configuration for the tmpfs type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size_bytes": { + Type: schema.TypeInt, + Description: "The size for the tmpfs mount in bytes", + Optional: true, + }, + "mode": { + Type: schema.TypeInt, + Description: "The permission mode for the tmpfs mount in an integer", + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "stop_signal": { + Type: schema.TypeString, + Description: "Signal to stop the container", + Optional: true, + }, + "stop_grace_period": { + Type: schema.TypeString, + Description: "Amount of time to wait for the container to terminate before forcefully removing it (ms|s|m|h)", + Optional: true, + Computed: true, + ValidateFunc: validateDurationGeq0(), + }, + "healthcheck": { + Type: schema.TypeList, + Description: "A test to perform to check that the container is healthy", + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeList, + Description: "The test to perform as list", + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "interval": { + Type: schema.TypeString, + Description: "Time between running the check (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "timeout": { + Type: schema.TypeString, + Description: "Maximum time to allow one check to run (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "start_period": { + Type: schema.TypeString, + Description: "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "retries": { + Type: schema.TypeInt, + Description: "Consecutive failures needed to report unhealthy", + Optional: true, + Default: 0, + ValidateFunc: validateIntegerGeqThan(0), + }, + }, + }, + }, + "hosts": { + Type: schema.TypeSet, + Description: "A list of hostname/IP mappings to add to the container's hosts file.", + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "host": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + "dns_config": { + Type: schema.TypeList, + Description: "Specification for DNS related configurations in resolver configuration file (resolv.conf)", + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nameservers": { + Type: schema.TypeList, + Description: "The IP addresses of the name servers", + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "search": { + Type: schema.TypeList, + Description: "A search list for host-name lookup", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "options": { + Type: schema.TypeList, + Description: "A list of internal resolver variables to be modified (e.g., debug, ndots:3, etc.)", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "secrets": { + Type: schema.TypeSet, + Description: "References to zero or more secrets that will be exposed to the service", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_id": { + Type: schema.TypeString, + Description: "ID of the specific secret that we're referencing", + Required: true, + }, + "secret_name": { + Type: schema.TypeString, + Description: "Name of the secret that this references, but this is just provided for lookup/display purposes. The config in the reference will be identified by its ID", + Optional: true, + }, + "file_name": { + Type: schema.TypeString, + Description: "Represents the final filename in the filesystem", + Required: true, + }, + }, + }, + }, + "configs": { + Type: schema.TypeSet, + Description: "References to zero or more configs that will be exposed to the service", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "config_id": { + Type: schema.TypeString, + Description: "ID of the specific config that we're referencing", + Required: true, + }, + "config_name": { + Type: schema.TypeString, + Description: "Name of the config that this references, but this is just provided for lookup/display purposes. The config in the reference will be identified by its ID", + Optional: true, + }, + "file_name": { + Type: schema.TypeString, + Description: "Represents the final filename in the filesystem", + Required: true, + }, + }, + }, + }, + "isolation": { + Type: schema.TypeString, + Description: "Isolation technology of the containers running the service. (Windows only)", + Optional: true, + Default: "default", + ValidateFunc: validateStringMatchesPattern(`^(default|process|hyperv)$`), + }, + }, + }, + }, + "resources": { + Type: schema.TypeList, + Description: "Resource requirements which apply to each individual container created as part of the service", + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "limits": { + Type: schema.TypeList, + Description: "Describes the resources which can be advertised by a node and requested by a task", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nano_cpus": { + Type: schema.TypeInt, + Description: "CPU shares in units of 1/1e9 (or 10^-9) of the CPU. Should be at least 1000000", + Optional: true, + }, + "memory_bytes": { + Type: schema.TypeInt, + Description: "The amounf of memory in bytes the container allocates", + Optional: true, + }, + "generic_resources": { + Type: schema.TypeList, + Description: "User-defined resources can be either Integer resources (e.g, SSD=3) or String resources (e.g, GPU=UUID1)", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "named_resources_spec": { + Type: schema.TypeSet, + Description: "The String resources", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "discrete_resources_spec": { + Type: schema.TypeSet, + Description: "The Integer resources", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, + }, + }, + }, + "reservation": { + Type: schema.TypeList, + Description: "An object describing the resources which can be advertised by a node and requested by a task", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nano_cpus": { + Description: "CPU shares in units of 1/1e9 (or 10^-9) of the CPU. Should be at least 1000000", + Type: schema.TypeInt, + Optional: true, + }, + "memory_bytes": { + Type: schema.TypeInt, + Description: "The amounf of memory in bytes the container allocates", + Optional: true, + }, + "generic_resources": { + Type: schema.TypeList, + Description: "User-defined resources can be either Integer resources (e.g, SSD=3) or String resources (e.g, GPU=UUID1)", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "named_resources_spec": { + Type: schema.TypeSet, + Description: "The String resources", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "discrete_resources_spec": { + Type: schema.TypeSet, + Description: "The Integer resources", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "restart_policy": { + Type: schema.TypeMap, + Description: "Specification for the restart policy which applies to containers created as part of this service.", + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "condition": { + Type: schema.TypeString, + Description: "Condition for restart", + Optional: true, + ValidateFunc: validateStringMatchesPattern(`^(none|on-failure|any)$`), + }, + "delay": { + Type: schema.TypeString, + Description: "Delay between restart attempts (ms|s|m|h)", + Optional: true, + ValidateFunc: validateDurationGeq0(), + }, + "max_attempts": { + Type: schema.TypeInt, + Description: "Maximum attempts to restart a given container before giving up (default value is 0, which is ignored)", + Optional: true, + ValidateFunc: validateIntegerGeqThan(0), + }, + "window": { + Type: schema.TypeString, + Description: "The time window used to evaluate the restart policy (default value is 0, which is unbounded) (ms|s|m|h)", + Optional: true, + ValidateFunc: validateDurationGeq0(), + }, + }, + }, + }, + "placement": { + Type: schema.TypeList, + Description: "The placement preferences", + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "constraints": { + Type: schema.TypeSet, + Description: "An array of constraints. e.g.: node.role==manager", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "prefs": { + Type: schema.TypeSet, + Description: "Preferences provide a way to make the scheduler aware of factors such as topology. They are provided in order from highest to lowest precedence, e.g.: spread=node.role.manager", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "platforms": { + Type: schema.TypeSet, + Description: "Platforms stores all the platforms that the service's image can run on", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "architecture": { + Type: schema.TypeString, + Description: "The architecture, e.g. amd64", + Required: true, + }, + "os": { + Type: schema.TypeString, + Description: "The operation system, e.g. linux", + Required: true, + }, + }, + }, + }, + }, + }, + }, + "force_update": { + Type: schema.TypeInt, + Description: "A counter that triggers an update even if no relevant parameters have been changed. See https://github.com/docker/swarmkit/blob/master/api/specs.proto#L126", + Optional: true, + Computed: true, + ValidateFunc: validateIntegerGeqThan(0), + }, + "runtime": { + Type: schema.TypeString, + Description: "Runtime is the type of runtime specified for the task executor. See https://github.com/moby/moby/blob/master/api/types/swarm/runtime.go", + Optional: true, + Computed: true, + ValidateFunc: validateStringMatchesPattern("^(container|plugin)$"), + }, + "networks": { + Type: schema.TypeSet, + Description: "Ids of the networks in which the container will be put in.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "log_driver": { + Type: schema.TypeList, + Description: "Specifies the log driver to use for tasks created from this spec. If not present, the default one for the swarm will be used, finally falling back to the engine default if not specified", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "The logging driver to use", + Required: true, + }, + "options": { + Type: schema.TypeMap, + Description: "The options for the logging driver", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "mode": { + Type: schema.TypeList, + Description: "Scheduling mode for the service", + MaxItems: 1, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "replicated": { + Type: schema.TypeList, + Description: "The replicated service mode", + MaxItems: 1, + Optional: true, + Computed: true, + ConflictsWith: []string{"mode.0.global"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "replicas": { + Type: schema.TypeInt, + Description: "The amount of replicas of the service", + Optional: true, + Default: 1, + ValidateFunc: validateIntegerGeqThan(1), + }, + }, + }, + }, + "global": { + Type: schema.TypeBool, + Description: "The global service mode", + Optional: true, + Default: false, + ConflictsWith: []string{"mode.0.replicated", "converge_config"}, + }, + }, + }, + }, + "update_config": { + Type: schema.TypeList, + Description: "Specification for the update strategy of the service", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "parallelism": { + Type: schema.TypeInt, + Description: "Maximum number of tasks to be updated in one iteration", + Optional: true, + Default: 1, + ValidateFunc: validateIntegerGeqThan(0), + }, + "delay": { + Type: schema.TypeString, + Description: "Delay between task updates (ns|us|ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "failure_action": { + Type: schema.TypeString, + Description: "Action on update failure: pause | continue | rollback", + Optional: true, + Default: "pause", + ValidateFunc: validateStringMatchesPattern("^(pause|continue|rollback)$"), + }, + "monitor": { + Type: schema.TypeString, + Description: "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)", + Optional: true, + Default: "5s", + ValidateFunc: validateDurationGeq0(), + }, + "max_failure_ratio": { + Type: schema.TypeString, + Description: "Failure rate to tolerate during an update", + Optional: true, + Default: "0.0", + ValidateFunc: validateStringIsFloatRatio(), + }, + "order": { + Type: schema.TypeString, + Description: "Update order: either 'stop-first' or 'start-first'", + Optional: true, + Default: "stop-first", + ValidateFunc: validateStringMatchesPattern("^(stop-first|start-first)$"), + }, + }, + }, + }, + "rollback_config": { + Type: schema.TypeList, + Description: "Specification for the rollback strategy of the service", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "parallelism": { + Type: schema.TypeInt, + Description: "Maximum number of tasks to be rollbacked in one iteration", + Optional: true, + Default: 1, + ValidateFunc: validateIntegerGeqThan(0), + }, + "delay": { + Type: schema.TypeString, + Description: "Delay between task rollbacks (ns|us|ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "failure_action": { + Type: schema.TypeString, + Description: "Action on rollback failure: pause | continue", + Optional: true, + Default: "pause", + ValidateFunc: validateStringMatchesPattern("(pause|continue)"), + }, + "monitor": { + Type: schema.TypeString, + Description: "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)", + Optional: true, + Default: "5s", + ValidateFunc: validateDurationGeq0(), + }, + "max_failure_ratio": { + Type: schema.TypeString, + Description: "Failure rate to tolerate during a rollback", + Optional: true, + Default: "0.0", + ValidateFunc: validateStringIsFloatRatio(), + }, + "order": { + Type: schema.TypeString, + Description: "Rollback order: either 'stop-first' or 'start-first'", + Optional: true, + Default: "stop-first", + ValidateFunc: validateStringMatchesPattern("(stop-first|start-first)"), + }, + }, + }, + }, + "endpoint_spec": { + Type: schema.TypeList, + Description: "Properties that can be configured to access and load balance a service", + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "mode": { + Type: schema.TypeString, + Description: "The mode of resolution to use for internal load balancing between tasks", + Optional: true, + Computed: true, + ValidateFunc: validateStringMatchesPattern(`^(vip|dnsrr)$`), + }, + "ports": { + Type: schema.TypeSet, + Description: "List of exposed ports that this service is accessible on from the outside. Ports can only be provided if 'vip' resolution mode is used.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "A random name for the port", + Optional: true, + }, + "protocol": { + Type: schema.TypeString, + Description: "Rrepresents the protocol of a port: 'tcp', 'udp' or 'sctp'", + Optional: true, + Default: "tcp", + ValidateFunc: validateStringMatchesPattern(`^(tcp|udp|sctp)$`), + }, + "target_port": { + Type: schema.TypeInt, + Description: "The port inside the container", + Required: true, + }, + "published_port": { + Type: schema.TypeInt, + Description: "The port on the swarm hosts.", + Optional: true, + }, + "publish_mode": { + Type: schema.TypeString, + Description: "Represents the mode in which the port is to be published: 'ingress' or 'host'", + Optional: true, + Default: "ingress", + ValidateFunc: validateStringMatchesPattern(`^(host|ingress)$`), + }, + }, + }, + }, + }, + }, + }, + "converge_config": { + Type: schema.TypeList, + Description: "A configuration to ensure that a service converges aka reaches the desired that of all task up and running", + MaxItems: 1, + Optional: true, + ConflictsWith: []string{"mode.0.global"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delay": { + Type: schema.TypeString, + Description: "The interval to check if the desired state is reached (ms|s). Default: 7s", + Optional: true, + Default: "7s", + ValidateFunc: validateDurationGeq0(), + }, + "timeout": { + Type: schema.TypeString, + Description: "The timeout of the service to reach the desired state (s|m). Default: 3m", + Optional: true, + Default: "3m", + ValidateFunc: validateDurationGeq0(), + }, + }, + }, + }, + }, + } +} + +func resourceDockerServiceV0() *schema.Resource { + return &schema.Resource{ Schema: map[string]*schema.Schema{ "auth": { Type: schema.TypeMap, diff --git a/docker/resource_docker_service_funcs.go b/docker/resource_docker_service_funcs.go index b310ba0e..d5a3a948 100644 --- a/docker/resource_docker_service_funcs.go +++ b/docker/resource_docker_service_funcs.go @@ -118,7 +118,7 @@ func resourceDockerServiceRead(d *schema.ResourceData, meta interface{}) error { d.SetId(service.ID) d.Set("name", service.Spec.Name) - d.Set("labels", service.Spec.Labels) + d.Set("labels", mapToLabelSet(service.Spec.Labels)) if err = d.Set("task_spec", flattenTaskSpec(service.Spec.TaskTemplate)); err != nil { log.Printf("[WARN] failed to set task spec from API: %s", err) @@ -607,7 +607,7 @@ func createServiceSpec(d *schema.ResourceData) (swarm.ServiceSpec, error) { // createServiceLabels creates the labels for the service func createServiceLabels(d *schema.ResourceData) (map[string]string, error) { if v, ok := d.GetOk("labels"); ok { - return mapTypeMapValsToString(v.(map[string]interface{})), nil + return labelSetToMap(v.(*schema.Set)), nil } return nil, nil } @@ -686,7 +686,7 @@ func createContainerSpec(v interface{}) (*swarm.ContainerSpec, error) { containerSpec.Image = value.(string) } if value, ok := rawContainerSpec["labels"]; ok { - containerSpec.Labels = mapTypeMapValsToString(value.(map[string]interface{})) + containerSpec.Labels = labelSetToMap(value.(*schema.Set)) } if value, ok := rawContainerSpec["command"]; ok { containerSpec.Command = stringListToStringSlice(value.([]interface{})) @@ -796,7 +796,7 @@ func createContainerSpec(v interface{}) (*swarm.ContainerSpec, error) { mountInstance.VolumeOptions.NoCopy = value.(bool) } if value, ok := rawVolumeOptions["labels"]; ok { - mountInstance.VolumeOptions.Labels = mapTypeMapValsToString(value.(map[string]interface{})) + mountInstance.VolumeOptions.Labels = labelSetToMap(value.(*schema.Set)) } // because it is not possible to nest maps if value, ok := rawVolumeOptions["driver_name"]; ok { diff --git a/docker/resource_docker_service_test.go b/docker/resource_docker_service_test.go index 17f70037..8311c49d 100644 --- a/docker/resource_docker_service_test.go +++ b/docker/resource_docker_service_test.go @@ -223,12 +223,18 @@ func TestAccDockerService_fullSpec(t *testing.T) { provider = "docker.private" name = "tftest-service-basic" + labels { + label = "servicelabel" + value = "true" + } + task_spec { container_spec { image = "127.0.0.1:15000/tftest-service:v1" - labels = { - foo = "bar" + labels { + label = "foo" + value = "bar" } command = ["ls"] @@ -264,8 +270,9 @@ func TestAccDockerService_fullSpec(t *testing.T) { volume_options { no_copy = true - labels = { - foo = "bar" + labels { + label = "foo" + value = "bar" } driver_name = "random-driver" driver_options = { @@ -393,8 +400,10 @@ func TestAccDockerService_fullSpec(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestMatchResourceAttr("docker_service.foo", "id", serviceIDRegex), resource.TestCheckResourceAttr("docker_service.foo", "name", "tftest-service-basic"), + testCheckLabelMap("docker_service.foo", "labels", map[string]string{"servicelabel": "true"}), resource.TestMatchResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.image", regexp.MustCompile(`127.0.0.1:15000/tftest-service:v1.*`)), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.foo", "bar"), + testCheckLabelMap("docker_service.foo", "task_spec.0.container_spec.0.labels", map[string]string{"foo": "bar"}), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.command.0", "ls"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.args.0", "-las"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hostname", "my-fancy-service"), @@ -415,7 +424,7 @@ func TestAccDockerService_fullSpec(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.type", "volume"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.read_only", "true"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.no_copy", "true"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.labels.foo", "bar"), + testCheckLabelMap("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.labels", map[string]string{"foo": "bar"}), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.driver_name", "random-driver"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.driver_options.op1", "val1"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_signal", "SIGTERM"), @@ -738,9 +747,13 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { type = "volume" read_only = true volume_options { - labels = { - env = "dev" - terraform = "true" + labels { + label = "env" + value = "dev" + } + labels { + label = "terraform" + value = "true" } } } @@ -781,9 +794,13 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { type = "volume" read_only = true volume_options { - labels = { - env = "dev" - terraform = "true" + labels { + label = "env" + value = "dev" + } + labels { + label = "terraform" + value = "true" } } } @@ -793,9 +810,13 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { type = "volume" read_only = true volume_options { - labels = { - env = "dev" - terraform = "true" + labels { + label = "env" + value = "dev" + } + labels { + label = "terraform" + value = "true" } } } @@ -877,7 +898,7 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.1878413705.host", "testhost"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.1878413705.ip", "10.0.1.0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.isolation", "default"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.%", "0"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.#", "1"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.privileges.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_grace_period", "10s"), @@ -925,7 +946,7 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.575059346.host", "testhost2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.575059346.ip", "10.0.2.2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.isolation", "default"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.%", "0"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.#", "2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.privileges.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_grace_period", "10s"), @@ -973,7 +994,7 @@ func TestAccDockerService_updateMultiplePropertiesConverge(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.575059346.host", "testhost2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.hosts.575059346.ip", "10.0.2.2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.isolation", "default"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.%", "0"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.#", "2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.privileges.#", "0"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_grace_period", "10s"), From b9d61dc5357cc1311fe6381dd77ba5f24db5f439 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Thu, 14 Nov 2019 17:10:48 -0500 Subject: [PATCH 14/19] some more migration --- docker/resource_docker_container.go | 776 ++++++++++++++++++++++- docker/resource_docker_container_test.go | 1 + docker/resource_docker_service.go | 2 + docker/structures_service.go | 4 +- 4 files changed, 780 insertions(+), 3 deletions(-) diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 91215abb..a42d6c93 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -13,7 +13,33 @@ func resourceDockerContainer() *schema.Resource { Update: resourceDockerContainerUpdate, Delete: resourceDockerContainerDelete, MigrateState: resourceDockerContainerMigrateState, - SchemaVersion: 1, + SchemaVersion: 2, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 1, + Type: resourceDockerContainerV1().CoreConfigSchema().ImpliedType(), + Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + labelMap := rawState["labels"].(map[string]interface{}) + rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + + mounts := rawState["mounts"].(*schema.Set).List() + newMounts := make([]interface{}, len(mounts)) + for i, mountI := range newMounts { + mount := mountI.(map[string]interface{}) + vo := mount["volume_options"].([]interface{})[0].(map[string]interface{}) + + labelMap = vo["labels"].(map[string]interface{}) + vo["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + + mount["volume_options"] = vo + newMounts[i] = mount + } + rawState["mounts"] = newMounts + + return rawState, nil + }, + }, + }, Schema: map[string]*schema.Schema{ "name": { @@ -760,6 +786,754 @@ func resourceDockerContainer() *schema.Resource { } } +func resourceDockerContainerV1() *schema.Resource { + return &schema.Resource{ + //This is only used for state migration, so the CRUD + //callbacks are no longer relevant + SchemaVersion: 1, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "rm": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + + "read_only": { + Type: schema.TypeBool, + Default: false, + Optional: true, + ForceNew: true, + }, + + "start": { + Type: schema.TypeBool, + Default: true, + Optional: true, + }, + + "attach": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + + "logs": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + + // Indicates whether the container must be running. + // + // An assumption is made that configured containers + // should be running; if not, they should not be in + // the configuration. Therefore a stopped container + // should be started. Set to false to have the + // provider leave the container alone. + // + // Actively-debugged containers are likely to be + // stopped and started manually, and Docker has + // some provisions for restarting containers that + // stop. The utility here comes from the fact that + // this will delete and re-create the container + // following the principle that the containers + // should be pristine when started. + "must_run": { + Type: schema.TypeBool, + Default: true, + Optional: true, + }, + + "exit_code": { + Type: schema.TypeInt, + Computed: true, + }, + + "container_logs": { + Type: schema.TypeString, + Computed: true, + }, + + // ForceNew is not true for image because we need to + // sane this against Docker image IDs, as each image + // can have multiple names/tags attached do it. + "image": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + // DiffSuppressFunc: suppressIfSHAwasAdded(), // TODO mvogel + }, + + "hostname": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "domainname": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "command": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "entrypoint": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "user": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "dns": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "dns_opts": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "dns_search": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "publish_all_ports": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "restart": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "no", + ValidateFunc: validateStringMatchesPattern(`^(no|on-failure|always|unless-stopped)$`), + }, + + "max_retry_count": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "working_dir": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "capabilities": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "drop": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, + "mounts": { + Type: schema.TypeSet, + Description: "Specification for mounts to be added to containers created as part of the service", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target": { + Type: schema.TypeString, + Description: "Container path", + Required: true, + }, + "source": { + Type: schema.TypeString, + Description: "Mount source (e.g. a volume name, a host path)", + Optional: true, + }, + "type": { + Type: schema.TypeString, + Description: "The mount type", + Required: true, + ValidateFunc: validateStringMatchesPattern(`^(bind|volume|tmpfs)$`), + }, + "read_only": { + Type: schema.TypeBool, + Description: "Whether the mount should be read-only", + Optional: true, + }, + "bind_options": { + Type: schema.TypeList, + Description: "Optional configuration for the bind type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "propagation": { + Type: schema.TypeString, + Description: "A propagation mode with the value", + Optional: true, + ValidateFunc: validateStringMatchesPattern(`^(private|rprivate|shared|rshared|slave|rslave)$`), + }, + }, + }, + }, + "volume_options": { + Type: schema.TypeList, + Description: "Optional configuration for the volume type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "no_copy": { + Type: schema.TypeBool, + Description: "Populate volume with data from the target", + Optional: true, + }, + "labels": { + Type: schema.TypeMap, + Description: "User-defined key/value metadata", + Optional: true, + }, + "driver_name": { + Type: schema.TypeString, + Description: "Name of the driver to use to create the volume.", + Optional: true, + }, + "driver_options": { + Type: schema.TypeMap, + Description: "key/value map of driver specific options", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tmpfs_options": { + Type: schema.TypeList, + Description: "Optional configuration for the tmpfs type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size_bytes": { + Type: schema.TypeInt, + Description: "The size for the tmpfs mount in bytes", + Optional: true, + }, + "mode": { + Type: schema.TypeInt, + Description: "The permission mode for the tmpfs mount in an integer", + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "volumes": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from_container": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "container_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "host_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateDockerContainerPath, + }, + + "volume_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "read_only": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "tmpfs": { + Type: schema.TypeMap, + Optional: true, + }, + "ports": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "internal": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "external": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "ip": { + Type: schema.TypeString, + Default: "0.0.0.0", + Optional: true, + ForceNew: true, + StateFunc: func(val interface{}) string { + // Empty IP assignments default to 0.0.0.0 + if val.(string) == "" { + return "0.0.0.0" + } + + return val.(string) + }, + }, + + "protocol": { + Type: schema.TypeString, + Default: "tcp", + Optional: true, + ForceNew: true, + }, + }, + }, + DiffSuppressFunc: suppressIfPortsDidNotChangeForMigrationV0ToV1(), + }, + + "host": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "host": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + + "ulimit": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "soft": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "hard": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + }, + }, + }, + + "env": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "links": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Deprecated: "The --link flag is a legacy feature of Docker. It may eventually be removed.", + }, + + "ip_address": { + Type: schema.TypeString, + Computed: true, + Deprecated: "Use ip_adresses_data instead. This field exposes the data of the container's first network.", + }, + + "ip_prefix_length": { + Type: schema.TypeInt, + Computed: true, + Deprecated: "Use ip_prefix_length from ip_adresses_data instead. This field exposes the data of the container's first network.", + }, + + "gateway": { + Type: schema.TypeString, + Computed: true, + Deprecated: "Use gateway from ip_adresses_data instead. This field exposes the data of the container's first network.", + }, + + "bridge": { + Type: schema.TypeString, + Computed: true, + }, + + "network_data": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "network_name": { + Type: schema.TypeString, + Computed: true, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "ip_prefix_length": { + Type: schema.TypeInt, + Computed: true, + }, + "gateway": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "privileged": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "devices": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host_path": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "container_path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "permissions": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + + "destroy_grace_seconds": { + Type: schema.TypeInt, + Optional: true, + }, + + "labels": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + + "memory": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(0), + }, + + "memory_swap": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(-1), + }, + + "shm_size": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(0), + }, + + "cpu_shares": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(0), + }, + + "cpu_set": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateStringMatchesPattern(`^\d+([,-]\d+)*$`), + }, + + "log_driver": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "json-file", + }, + + "log_opts": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + + "network_alias": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Description: "Set an alias for the container in all specified networks", + Deprecated: "Use networks_advanced instead. Will be removed in v2.0.0", + }, + + "network_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "networks": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Deprecated: "Use networks_advanced instead. Will be removed in v2.0.0", + }, + + "networks_advanced": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "aliases": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ipv4_address": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ipv6_address": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + + "pid_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "userns_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "upload": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Optional: true, + // This is intentional. The container is mutated once, and never updated later. + // New configuration forces a new deployment, even with the same binaries. + ForceNew: true, + }, + "content_base64": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateStringIsBase64Encoded(), + }, + "file": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "executable": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + }, + }, + }, + + "healthcheck": { + Type: schema.TypeList, + Description: "A test to perform to check that the container is healthy", + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeList, + Description: "The test to perform as list", + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "interval": { + Type: schema.TypeString, + Description: "Time between running the check (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "timeout": { + Type: schema.TypeString, + Description: "Maximum time to allow one check to run (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "start_period": { + Type: schema.TypeString, + Description: "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)", + Optional: true, + Default: "0s", + ValidateFunc: validateDurationGeq0(), + }, + "retries": { + Type: schema.TypeInt, + Description: "Consecutive failures needed to report unhealthy", + Optional: true, + Default: 0, + ValidateFunc: validateIntegerGeqThan(0), + }, + }, + }, + }, + + "sysctls": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + "ipc_mode": { + Type: schema.TypeString, + Description: "IPC sharing mode for the container", + Optional: true, + ForceNew: true, + }, + "group_add": { + Type: schema.TypeSet, + Description: "Additional groups for the container user", + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + func suppressIfPortsDidNotChangeForMigrationV0ToV1() schema.SchemaDiffSuppressFunc { return func(k, old, new string, d *schema.ResourceData) bool { if k == "ports.#" && old != new { diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index 587fab29..f008f7b1 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -551,6 +551,7 @@ func TestAccDockerContainer_customized(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccContainerRunning("docker_container.foo", &c), testCheck, + testCheckLabelMap("docker_container.foo", "labels", map[string]string{"env": "prod", "role": "test"}), ), }, }, diff --git a/docker/resource_docker_service.go b/docker/resource_docker_service.go index 038ed32e..2373864c 100644 --- a/docker/resource_docker_service.go +++ b/docker/resource_docker_service.go @@ -906,6 +906,8 @@ func resourceDockerService() *schema.Resource { func resourceDockerServiceV0() *schema.Resource { return &schema.Resource{ + //This is only used for state migration, so the CRUD + //callbacks are no longer relevant Schema: map[string]*schema.Schema{ "auth": { Type: schema.TypeMap, diff --git a/docker/structures_service.go b/docker/structures_service.go index 1a4e8ebb..18485f8b 100644 --- a/docker/structures_service.go +++ b/docker/structures_service.go @@ -103,7 +103,7 @@ func flattenContainerSpec(in *swarm.ContainerSpec) []interface{} { m["image"] = in.Image } if len(in.Labels) > 0 { - m["labels"] = in.Labels + m["labels"] = mapToLabelSet(in.Labels) } if len(in.Command) > 0 { m["command"] = in.Command @@ -219,7 +219,7 @@ func flattenServiceMounts(in []mount.Mount) *schema.Set { volumeOptionsItem := make(map[string]interface{}, 0) volumeOptionsItem["no_copy"] = v.VolumeOptions.NoCopy - volumeOptionsItem["labels"] = mapStringStringToMapStringInterface(v.VolumeOptions.Labels) + volumeOptionsItem["labels"] = mapToLabelSet(v.VolumeOptions.Labels) if v.VolumeOptions.DriverConfig != nil { if len(v.VolumeOptions.DriverConfig.Name) > 0 { volumeOptionsItem["driver_name"] = v.VolumeOptions.DriverConfig.Name From d878091f95a384f80b40ca4ef4b7ae4d3e7c4095 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Thu, 14 Nov 2019 17:44:58 -0500 Subject: [PATCH 15/19] refactor migration code and fix a couple tests --- docker/label.go | 27 +++++++---------- docker/label_migration.go | 40 ++++++++++++++++++++++++++ docker/resource_docker_container.go | 21 +++----------- docker/resource_docker_network.go | 5 +--- docker/resource_docker_secret.go | 5 +--- docker/resource_docker_service.go | 10 +++++++ docker/resource_docker_service_test.go | 1 - docker/resource_docker_volume.go | 5 +--- docker/resource_docker_volume_test.go | 4 +-- 9 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 docker/label_migration.go diff --git a/docker/label.go b/docker/label.go index b7213ba9..dc49ac5c 100644 --- a/docker/label.go +++ b/docker/label.go @@ -33,6 +33,17 @@ func hashStringLabel(str string) int { return schema.HashString(str) } +func mapStringInterfaceToLabelSet(labels map[string]interface{}) *schema.Set { + var mapped []interface{} + for k, v := range labels { + mapped = append(mapped, map[string]interface{}{ + "label": k, + "value": fmt.Sprintf("%v", v), + }) + } + return schema.NewSet(hashLabel, mapped) +} + func mapToLabelSet(labels map[string]string) *schema.Set { var mapped []interface{} for k, v := range labels { @@ -59,22 +70,6 @@ var labelSchema = &schema.Resource{ }, } -//unfortunately, _one_ of the several place that the old label schema was -//used specified that the keys had to be strings, while the others allowed -//any type of key and coerced them into strings. -func upgradeLabelMapFromV0ToV1(labelMap map[string]interface{}) []map[string]string { - var migratedState []map[string]string - - for l, v := range labelMap { - migratedState = append(migratedState, map[string]string{ - "label": l, - "value": fmt.Sprintf("%v", v), - }) - } - - return migratedState -} - //gatherImmediateSubkeys given an incomplete attribute identifier, find all //the strings (if any) that appear after this one in the various dot-separated //identifiers. diff --git a/docker/label_migration.go b/docker/label_migration.go new file mode 100644 index 00000000..280d4459 --- /dev/null +++ b/docker/label_migration.go @@ -0,0 +1,40 @@ +package docker + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func replaceLabelsMapFieldWithSetField(rawState map[string]interface{}) map[string]interface{} { + labelMap := rawState["labels"].(map[string]interface{}) + rawState["labels"] = mapStringInterfaceToLabelSet(labelMap) + + return rawState +} + +func migrateContainerLabels(rawState map[string]interface{}) map[string]interface{} { + rawState = replaceLabelsMapFieldWithSetField(rawState) + + mounts := rawState["mounts"].(*schema.Set).List() + newMounts := make([]interface{}, len(mounts)) + for i, mountI := range newMounts { + mount := mountI.(map[string]interface{}) + volumeOptions := mount["volume_options"].([]interface{})[0].(map[string]interface{}) + + mount["volume_options"] = replaceLabelsMapFieldWithSetField(volumeOptions) + newMounts[i] = mount + } + rawState["mounts"] = newMounts + + return rawState +} + +func migrateServiceLabels(rawState map[string]interface{}) map[string]interface{} { + rawState = replaceLabelsMapFieldWithSetField(rawState) + + taskSpec := rawState["task_spec"].([]interface{})[0].(map[string]interface{}) + containerSpec := taskSpec["container_spec"].([]interface{})[0].(map[string]interface{}) + taskSpec["container_spec"] = migrateContainerLabels(containerSpec) + + rawState["task_spec"] = taskSpec + return rawState +} diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index a42d6c93..089fbf24 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -19,24 +19,11 @@ func resourceDockerContainer() *schema.Resource { Version: 1, Type: resourceDockerContainerV1().CoreConfigSchema().ImpliedType(), Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - labelMap := rawState["labels"].(map[string]interface{}) - rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) + //TODO do the ohter V0-to-V1 migration, unless we're okay + //with breaking for users who straggled on their docker + //provider version - mounts := rawState["mounts"].(*schema.Set).List() - newMounts := make([]interface{}, len(mounts)) - for i, mountI := range newMounts { - mount := mountI.(map[string]interface{}) - vo := mount["volume_options"].([]interface{})[0].(map[string]interface{}) - - labelMap = vo["labels"].(map[string]interface{}) - vo["labels"] = upgradeLabelMapFromV0ToV1(labelMap) - - mount["volume_options"] = vo - newMounts[i] = mount - } - rawState["mounts"] = newMounts - - return rawState, nil + return migrateContainerLabels(rawState), nil }, }, }, diff --git a/docker/resource_docker_network.go b/docker/resource_docker_network.go index 9d4ebba3..b028ec7f 100644 --- a/docker/resource_docker_network.go +++ b/docker/resource_docker_network.go @@ -125,10 +125,7 @@ func resourceDockerNetwork() *schema.Resource { Version: 0, Type: resourceDockerNetworkV0().CoreConfigSchema().ImpliedType(), Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - labelMap := rawState["labels"].(map[string]interface{}) - rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) - - return rawState, nil + return replaceLabelsMapFieldWithSetField(rawState), nil }, }, }, diff --git a/docker/resource_docker_secret.go b/docker/resource_docker_secret.go index 35c8f6bb..f3bfca69 100644 --- a/docker/resource_docker_secret.go +++ b/docker/resource_docker_secret.go @@ -46,10 +46,7 @@ func resourceDockerSecret() *schema.Resource { Version: 0, Type: resourceDockerSecretV0().CoreConfigSchema().ImpliedType(), Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - labelMap := rawState["labels"].(map[string]interface{}) - rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) - - return rawState, nil + return replaceLabelsMapFieldWithSetField(rawState), nil }, }, }, diff --git a/docker/resource_docker_service.go b/docker/resource_docker_service.go index 2373864c..c3578e08 100644 --- a/docker/resource_docker_service.go +++ b/docker/resource_docker_service.go @@ -901,6 +901,16 @@ func resourceDockerService() *schema.Resource { }, }, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceDockerServiceV0().CoreConfigSchema().ImpliedType(), + Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + return migrateServiceLabels(rawState), nil + }, + }, + }, } } diff --git a/docker/resource_docker_service_test.go b/docker/resource_docker_service_test.go index 8311c49d..3ea3f583 100644 --- a/docker/resource_docker_service_test.go +++ b/docker/resource_docker_service_test.go @@ -402,7 +402,6 @@ func TestAccDockerService_fullSpec(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "name", "tftest-service-basic"), testCheckLabelMap("docker_service.foo", "labels", map[string]string{"servicelabel": "true"}), resource.TestMatchResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.image", regexp.MustCompile(`127.0.0.1:15000/tftest-service:v1.*`)), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.labels.foo", "bar"), testCheckLabelMap("docker_service.foo", "task_spec.0.container_spec.0.labels", map[string]string{"foo": "bar"}), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.command.0", "ls"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.args.0", "-las"), diff --git a/docker/resource_docker_volume.go b/docker/resource_docker_volume.go index a4006edc..15719e03 100644 --- a/docker/resource_docker_volume.go +++ b/docker/resource_docker_volume.go @@ -54,10 +54,7 @@ func resourceDockerVolume() *schema.Resource { Version: 0, Type: resourceDockerVolumeV0().CoreConfigSchema().ImpliedType(), Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - labelMap := rawState["labels"].(map[string]interface{}) - rawState["labels"] = upgradeLabelMapFromV0ToV1(labelMap) - - return rawState, nil + return replaceLabelsMapFieldWithSetField(rawState), nil }, }, }, diff --git a/docker/resource_docker_volume_test.go b/docker/resource_docker_volume_test.go index 5898332c..fd3256f8 100644 --- a/docker/resource_docker_volume_test.go +++ b/docker/resource_docker_volume_test.go @@ -94,11 +94,11 @@ func testAccVolumeLabel(volume *types.Volume, name string, value string) resourc const testAccDockerVolumeLabelsConfig = ` resource "docker_volume" "foo" { name = "test_foo" - labels = { + labels { label = "com.docker.compose.project" value = "test" } - labels = { + labels { label = "com.docker.compose.volume" value = "foo" } From aa198607268c4c032fe71b7db04f6f0f4a5de161 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 15 Nov 2019 00:08:45 -0500 Subject: [PATCH 17/19] fix service test mount set key --- docker/resource_docker_service_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/resource_docker_service_test.go b/docker/resource_docker_service_test.go index 3ea3f583..5fa6b56e 100644 --- a/docker/resource_docker_service_test.go +++ b/docker/resource_docker_service_test.go @@ -418,14 +418,14 @@ func TestAccDockerService_fullSpec(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.privileges.0.se_linux_context.0.type", "type-label"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.privileges.0.se_linux_context.0.level", "level-label"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.read_only", "true"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.target", "/mount/test"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.source", "tftest-volume"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.type", "volume"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.read_only", "true"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.no_copy", "true"), - testCheckLabelMap("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.labels", map[string]string{"foo": "bar"}), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.driver_name", "random-driver"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.816078185.volume_options.0.driver_options.op1", "val1"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.target", "/mount/test"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.source", "tftest-volume"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.type", "volume"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.read_only", "true"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.volume_options.0.no_copy", "true"), + testCheckLabelMap("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.volume_options.0.labels", map[string]string{"foo": "bar"}), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.volume_options.0.driver_name", "random-driver"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.mounts.2817186635.volume_options.0.driver_options.op1", "val1"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_signal", "SIGTERM"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.stop_grace_period", "10s"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.container_spec.0.healthcheck.0.test.0", "CMD"), From e990b2db03703c44072112b3e3614f7436134334 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Fri, 15 Nov 2019 21:33:05 -0500 Subject: [PATCH 18/19] some debugging --- docker/label.go | 4 ++-- docker/label_migration.go | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docker/label.go b/docker/label.go index dc49ac5c..c95a5b61 100644 --- a/docker/label.go +++ b/docker/label.go @@ -33,7 +33,7 @@ func hashStringLabel(str string) int { return schema.HashString(str) } -func mapStringInterfaceToLabelSet(labels map[string]interface{}) *schema.Set { +func mapStringInterfaceToLabelList(labels map[string]interface{}) []interface{} { var mapped []interface{} for k, v := range labels { mapped = append(mapped, map[string]interface{}{ @@ -41,7 +41,7 @@ func mapStringInterfaceToLabelSet(labels map[string]interface{}) *schema.Set { "value": fmt.Sprintf("%v", v), }) } - return schema.NewSet(hashLabel, mapped) + return mapped } func mapToLabelSet(labels map[string]string) *schema.Set { diff --git a/docker/label_migration.go b/docker/label_migration.go index 280d4459..8b96f151 100644 --- a/docker/label_migration.go +++ b/docker/label_migration.go @@ -1,12 +1,13 @@ package docker -import ( - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" -) - func replaceLabelsMapFieldWithSetField(rawState map[string]interface{}) map[string]interface{} { - labelMap := rawState["labels"].(map[string]interface{}) - rawState["labels"] = mapStringInterfaceToLabelSet(labelMap) + labelMapIFace := rawState["labels"] + if labelMapIFace != nil { + labelMap := labelMapIFace.(map[string]interface{}) + rawState["labels"] = mapStringInterfaceToLabelList(labelMap) + } else { + rawState["labels"] = []interface{}{} + } return rawState } @@ -14,13 +15,15 @@ func replaceLabelsMapFieldWithSetField(rawState map[string]interface{}) map[stri func migrateContainerLabels(rawState map[string]interface{}) map[string]interface{} { rawState = replaceLabelsMapFieldWithSetField(rawState) - mounts := rawState["mounts"].(*schema.Set).List() + mounts := rawState["mounts"].([]interface{}) newMounts := make([]interface{}, len(mounts)) - for i, mountI := range newMounts { + for i, mountI := range mounts { mount := mountI.(map[string]interface{}) - volumeOptions := mount["volume_options"].([]interface{})[0].(map[string]interface{}) + volumeOptionsList := mount["volume_options"].([]interface{}) - mount["volume_options"] = replaceLabelsMapFieldWithSetField(volumeOptions) + if len(volumeOptionsList) != 0 { + mount["volume_options"] = replaceLabelsMapFieldWithSetField(volumeOptionsList[0].(map[string]interface{})) + } newMounts[i] = mount } rawState["mounts"] = newMounts @@ -33,8 +36,7 @@ func migrateServiceLabels(rawState map[string]interface{}) map[string]interface{ taskSpec := rawState["task_spec"].([]interface{})[0].(map[string]interface{}) containerSpec := taskSpec["container_spec"].([]interface{})[0].(map[string]interface{}) - taskSpec["container_spec"] = migrateContainerLabels(containerSpec) + migrateContainerLabels(containerSpec) - rawState["task_spec"] = taskSpec return rawState } From 4d158b4ba005b5cb397e25a11643fa04e52660a9 Mon Sep 17 00:00:00 2001 From: Xander Flood Date: Sat, 16 Nov 2019 09:11:17 -0500 Subject: [PATCH 19/19] tests for label migration --- docker/label_migration.go | 6 +- docker/label_migration_test.go | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 docker/label_migration_test.go diff --git a/docker/label_migration.go b/docker/label_migration.go index 8b96f151..fff03a14 100644 --- a/docker/label_migration.go +++ b/docker/label_migration.go @@ -13,7 +13,7 @@ func replaceLabelsMapFieldWithSetField(rawState map[string]interface{}) map[stri } func migrateContainerLabels(rawState map[string]interface{}) map[string]interface{} { - rawState = replaceLabelsMapFieldWithSetField(rawState) + replaceLabelsMapFieldWithSetField(rawState) mounts := rawState["mounts"].([]interface{}) newMounts := make([]interface{}, len(mounts)) @@ -22,7 +22,7 @@ func migrateContainerLabels(rawState map[string]interface{}) map[string]interfac volumeOptionsList := mount["volume_options"].([]interface{}) if len(volumeOptionsList) != 0 { - mount["volume_options"] = replaceLabelsMapFieldWithSetField(volumeOptionsList[0].(map[string]interface{})) + replaceLabelsMapFieldWithSetField(volumeOptionsList[0].(map[string]interface{})) } newMounts[i] = mount } @@ -32,7 +32,7 @@ func migrateContainerLabels(rawState map[string]interface{}) map[string]interfac } func migrateServiceLabels(rawState map[string]interface{}) map[string]interface{} { - rawState = replaceLabelsMapFieldWithSetField(rawState) + replaceLabelsMapFieldWithSetField(rawState) taskSpec := rawState["task_spec"].([]interface{})[0].(map[string]interface{}) containerSpec := taskSpec["container_spec"].([]interface{})[0].(map[string]interface{}) diff --git a/docker/label_migration_test.go b/docker/label_migration_test.go new file mode 100644 index 00000000..a5d7d680 --- /dev/null +++ b/docker/label_migration_test.go @@ -0,0 +1,102 @@ +package docker + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestMigrateServiceLabelState_empty_labels(t *testing.T) { + v0State := map[string]interface{}{ + "name": "volume-name", + "task_spec": []interface{}{ + map[string]interface{}{ + "container_spec": []interface{}{ + map[string]interface{}{ + "image": "repo:tag", + "mounts": []interface{}{ + map[string]interface{}{ + "target": "path/to/target", + "type": "bind", + "volume_options": []interface{}{ + map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + }, + } + + //first validate that we build that correctly + v0Config := terraform.NewResourceConfigRaw(v0State) + warns, errs := resourceDockerServiceV0().Validate(v0Config) + if len(warns) > 0 || len(errs) > 0 { + t.Error("test precondition failed - attempt to migrate an invalid v0 config") + return + } + + v1State := migrateServiceLabels(v0State) + v1Config := terraform.NewResourceConfigRaw(v1State) + warns, errs = resourceDockerService().Validate(v1Config) + if len(warns) > 0 || len(errs) > 0 { + fmt.Println(warns, errs) + t.Error("migrated service config is invalid") + return + } +} + +func TestMigrateServiceLabelState_with_labels(t *testing.T) { + v0State := map[string]interface{}{ + "name": "volume-name", + "task_spec": []interface{}{ + map[string]interface{}{ + "container_spec": []interface{}{ + map[string]interface{}{ + "image": "repo:tag", + "labels": map[string]interface{}{ + "type": "container", + "env": "dev", + }, + "mounts": []interface{}{ + map[string]interface{}{ + "target": "path/to/target", + "type": "bind", + "volume_options": []interface{}{ + map[string]interface{}{ + "labels": map[string]interface{}{ + "type": "mount", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "labels": map[string]interface{}{ + "foo": "bar", + "env": "dev", + }, + } + + //first validate that we build that correctly + v0Config := terraform.NewResourceConfigRaw(v0State) + warns, errs := resourceDockerServiceV0().Validate(v0Config) + if len(warns) > 0 || len(errs) > 0 { + t.Error("test precondition failed - attempt to migrate an invalid v0 config") + return + } + + v1State := migrateServiceLabels(v0State) + v1Config := terraform.NewResourceConfigRaw(v1State) + warns, errs = resourceDockerService().Validate(v1Config) + if len(warns) > 0 || len(errs) > 0 { + fmt.Println(warns, errs) + t.Error("migrated service config is invalid") + return + } +}