Repository: kreuzwerker/terraform-provider-docker

Issue:
label changes should not trigger a recreate on services
<!--- Please keep this note for the community --->

### Community Note

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request
* Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
* If you are interested in working on this issue or have submitted a pull request, please leave a comment

<!--- Thank you for keeping this note for the community --->

### Terraform (and docker Provider) Version

<!--- Please run `terraform -v` to show the Terraform core version and provider version(s). If you are not running the latest version of Terraform or the provider, please upgrade because your issue may have already been fixed. [Terraform documentation on provider versioning](https://www.terraform.io/docs/configuration/providers.html#provider-versions). --->

### Affected Resource(s)

<!--- Please list the affected resources and data sources. --->

* `docker_service`

### Terraform Configuration Files

<!--- Information about code formatting: https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code --->

```hcl
# nothing really because I am removing the existing stack deploy labels
```

### Debug Output

<!---
Please provide a link to a GitHub Gist containing the complete debug output. Please do NOT paste the debug output in the issue; just paste a link to the Gist.

To obtain the debug output, see the [Terraform documentation on debugging](https://www.terraform.io/docs/internals/debugging.html).
--->

### Panic Output

<!--- If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. --->

### Expected Behaviour

With docker cli I can do label-add and label-rm without recreating the container provided that the label was on the service level.

### Actual Behaviour

Triggers a drop and create

### Steps to Reproduce

<!--- Please list the steps required to reproduce the issue. --->

1. `terraform apply`

### Important Factoids

<!--- Are there anything atypical about your setup that we should know? For example: Running the docker daemon with TLS? --->

### References

<!---
Information about referencing Github Issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests

Are there any other GitHub issues (open or closed) or pull requests that should be linked here? Such as vendor documentation?
--->

* #0000


Hints from issue comments:
@Junkern: Thanks for the bug report. I will provide a fix for this in a bit :)

Solution patch:
diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go
index 981414c..23521bc 100644
--- a/internal/provider/helpers.go
+++ b/internal/provider/helpers.go
@@ -56,23 +56,28 @@ func mapToLabelSet(labels map[string]string) *schema.Set {
 	return schema.NewSet(hashLabel, mapped)
 }
 
-var labelSchema = &schema.Resource{
-	Schema: map[string]*schema.Schema{
-		"label": {
-			Type:        schema.TypeString,
-			Description: "Name of the label",
-			Required:    true,
-			ForceNew:    true,
+func newLabelSchema(forceNew bool) *schema.Resource {
+	return &schema.Resource{
+		Schema: map[string]*schema.Schema{
+			"label": {
+				Type:        schema.TypeString,
+				Description: "Name of the label",
+				Required:    true,
+				ForceNew:    forceNew,
+			},
+			"value": {
+				Type:        schema.TypeString,
+				Description: "Value of the label",
+				Required:    true,
+				ForceNew:    forceNew,
+			},
 		},
-		"value": {
-			Type:        schema.TypeString,
-			Description: "Value of the label",
-			Required:    true,
-			ForceNew:    true,
-		},
-	},
+	}
 }
 
+var labelSchema = newLabelSchema(true)
+var labelSchemaUpdatable = newLabelSchema(false)
+
 // 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/internal/provider/resource_docker_service.go b/internal/provider/resource_docker_service.go
index 8b80e32..c4f3069 100644
--- a/internal/provider/resource_docker_service.go
+++ b/internal/provider/resource_docker_service.go
@@ -62,7 +62,7 @@ func resourceDockerService() *schema.Resource {
 				Description: "User-defined key/value metadata",
 				Optional:    true,
 				Computed:    true,
-				Elem:        labelSchema,
+				Elem:        labelSchemaUpdatable,
 			},
 			"task_spec": {
 				Type:        schema.TypeList,



Test patch:
diff --git a/internal/provider/resource_docker_service_test.go b/internal/provider/resource_docker_service_test.go
index 55b48d9..e809742 100644
--- a/internal/provider/resource_docker_service_test.go
+++ b/internal/provider/resource_docker_service_test.go
@@ -317,6 +317,54 @@ func TestAccDockerService_minimalSpec(t *testing.T) {
 	})
 }
 
+func TestAccDockerService_updateLabels(t *testing.T) {
+	ctx := context.Background()
+	var serviceID string
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:          func() { testAccPreCheck(t) },
+		ProviderFactories: providerFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_service", "testAccDockerServiceUpdateLabels"), "prod"),
+				Check: resource.ComposeTestCheckFunc(
+					func(state *terraform.State) error {
+						rs, ok := state.RootModule().Resources["docker_service.foo"]
+						if !ok {
+							return fmt.Errorf("service resource not found in state")
+						}
+						if rs.Primary.ID == "" {
+							return fmt.Errorf("service id not set")
+						}
+						serviceID = rs.Primary.ID
+						return nil
+					},
+					testCheckLabelMap("docker_service.foo", "labels", map[string]string{"env": "prod"}),
+				),
+			},
+			{
+				Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_service", "testAccDockerServiceUpdateLabels"), "staging"),
+				Check: resource.ComposeTestCheckFunc(
+					func(state *terraform.State) error {
+						rs, ok := state.RootModule().Resources["docker_service.foo"]
+						if !ok {
+							return fmt.Errorf("service resource not found in state")
+						}
+						if rs.Primary.ID != serviceID {
+							return fmt.Errorf("expected service to be updated in place, but ID changed from %s to %s", serviceID, rs.Primary.ID)
+						}
+						return nil
+					},
+					testCheckLabelMap("docker_service.foo", "labels", map[string]string{"env": "staging"}),
+				),
+			},
+		},
+		CheckDestroy: func(state *terraform.State) error {
+			return checkAndRemoveImages(ctx, state)
+		},
+	})
+}
+
 func TestAccDockerService_fullSpec(t *testing.T) {
 	var s swarm.Service
 
diff --git a/testdata/resources/docker_service/testAccDockerServiceUpdateLabels.tf b/testdata/resources/docker_service/testAccDockerServiceUpdateLabels.tf
new file mode 100644
index 0000000..87ea55f
--- /dev/null
+++ b/testdata/resources/docker_service/testAccDockerServiceUpdateLabels.tf
@@ -0,0 +1,22 @@
+provider "docker" {
+  registry_auth {
+    address = "127.0.0.1:15000"
+  }
+}
+
+resource "docker_service" "foo" {
+  name = "tftest-service-labels"
+
+  labels {
+    label = "env"
+    value = "%s"
+  }
+
+  task_spec {
+    container_spec {
+      image             = "127.0.0.1:15000/tftest-service:v1@sha256:2ca4c7a50df3515ea96106caab374759879830f6e4d6b400cee064e2e8db08c0"
+      stop_grace_period = "10s"
+    }
+  }
+}
+