From a5332be18d8d9e218252eb5d69700eaf7a3b8e2a Mon Sep 17 00:00:00 2001 From: Manuel Vogel Date: Wed, 4 Jan 2023 14:03:13 +0100 Subject: [PATCH] feat(service): add alias for networks (#241) * feat(service): outlines alias for networks * feat: Add driver_opts sub-attribute. * fix: network driver options type conversions. * fix: Temporarily fix docker_service tests. Co-authored-by: Martin Co-authored-by: Martin Wentzel --- docs/resources/service.md | 16 +++- .../resource_docker_container_structures.go | 15 ++++ internal/provider/resource_docker_service.go | 43 +++++++++-- .../provider/resource_docker_service_funcs.go | 2 +- .../resource_docker_service_structures.go | 74 ++++++++++++++++++- .../provider/resource_docker_service_test.go | 16 ++-- .../testAccDockerServiceFullSpec.tf | 9 ++- 7 files changed, 159 insertions(+), 16 deletions(-) diff --git a/docs/resources/service.md b/docs/resources/service.md index fa4fd308..f66a3c10 100644 --- a/docs/resources/service.md +++ b/docs/resources/service.md @@ -345,7 +345,8 @@ Optional: - `force_update` (Number) A counter that triggers an update even if no relevant parameters have been changed. See the [spec](https://github.com/docker/swarmkit/blob/master/api/specs.proto#L126). - `log_driver` (Block List, Max: 1) 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 (see [below for nested schema](#nestedblock--task_spec--log_driver)) -- `networks` (Set of String) Ids of the networks in which the container will be put in +- `networks` (Set of String, Deprecated) Ids of the networks in which the container will be put in +- `networks_advanced` (Block Set) The networks the container is attached to (see [below for nested schema](#nestedblock--task_spec--networks_advanced)) - `placement` (Block List, Max: 1) The placement preferences (see [below for nested schema](#nestedblock--task_spec--placement)) - `resources` (Block List, Max: 1) Resource requirements which apply to each individual container created as part of the service (see [below for nested schema](#nestedblock--task_spec--resources)) - `restart_policy` (Block List, Max: 1) Specification for the restart policy which applies to containers created as part of this service. (see [below for nested schema](#nestedblock--task_spec--restart_policy)) @@ -556,6 +557,19 @@ Optional: - `options` (Map of String) The options for the logging driver + +### Nested Schema for `task_spec.networks_advanced` + +Required: + +- `name` (String) The name/id of the network. + +Optional: + +- `aliases` (Set of String) The network aliases of the container in the specific network. +- `driver_opts` (Set of String) An array of driver options for the network, e.g. `opts1=value` + + ### Nested Schema for `task_spec.placement` diff --git a/internal/provider/resource_docker_container_structures.go b/internal/provider/resource_docker_container_structures.go index f8678f55..e696132d 100644 --- a/internal/provider/resource_docker_container_structures.go +++ b/internal/provider/resource_docker_container_structures.go @@ -102,6 +102,21 @@ func stringSetToStringSlice(stringSet *schema.Set) []string { return ret } +func stringSetToMapStringString(stringSet *schema.Set) map[string]string { + ret := map[string]string{} + if stringSet == nil { + return ret + } + for _, envVal := range stringSet.List() { + envValSplit := strings.SplitN(envVal.(string), "=", 2) + if len(envValSplit) != 2 { + continue + } + ret[envValSplit[0]] = envValSplit[1] + } + return ret +} + func mapTypeMapValsToString(typeMap map[string]interface{}) map[string]string { mapped := make(map[string]string, len(typeMap)) for k, v := range typeMap { diff --git a/internal/provider/resource_docker_service.go b/internal/provider/resource_docker_service.go index d9bdb783..05dd41f8 100644 --- a/internal/provider/resource_docker_service.go +++ b/internal/provider/resource_docker_service.go @@ -681,11 +681,44 @@ func resourceDockerService() *schema.Resource { ValidateDiagFunc: 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, + 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, + Deprecated: "Use networks_advanced instead", + ConflictsWith: []string{"task_spec.0.networks_advanced"}, + }, + "networks_advanced": { + Type: schema.TypeSet, + Description: "The networks the container is attached to", + Optional: true, + ConflictsWith: []string{"task_spec.0.networks"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "The name/id of the network.", + Required: true, + ForceNew: true, + }, + "aliases": { + Type: schema.TypeSet, + Description: "The network aliases of the container in the specific network.", + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "driver_opts": { + Type: schema.TypeSet, + Description: "An array of driver options for the network, e.g. `opts1=value`", + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, }, "log_driver": { Type: schema.TypeList, diff --git a/internal/provider/resource_docker_service_funcs.go b/internal/provider/resource_docker_service_funcs.go index ca3fd813..333b5849 100644 --- a/internal/provider/resource_docker_service_funcs.go +++ b/internal/provider/resource_docker_service_funcs.go @@ -131,7 +131,7 @@ func resourceDockerServiceReadRefreshFunc(ctx context.Context, d.Set("name", service.Spec.Name) d.Set("labels", mapToLabelSet(service.Spec.Labels)) - if err = d.Set("task_spec", flattenTaskSpec(service.Spec.TaskTemplate)); err != nil { + if err = d.Set("task_spec", flattenTaskSpec(service.Spec.TaskTemplate, d)); err != nil { log.Printf("[WARN] failed to set task spec from API: %s", err) } if err = d.Set("mode", flattenServiceMode(service.Spec.Mode)); err != nil { diff --git a/internal/provider/resource_docker_service_structures.go b/internal/provider/resource_docker_service_structures.go index 348d1a7d..04a8dfdb 100644 --- a/internal/provider/resource_docker_service_structures.go +++ b/internal/provider/resource_docker_service_structures.go @@ -18,7 +18,7 @@ import ( // flatten API objects to the terraform schema // //////////// // see https://learn.hashicorp.com/tutorials/terraform/provider-create?in=terraform/providers#add-flattening-functions -func flattenTaskSpec(in swarm.TaskSpec) []interface{} { +func flattenTaskSpec(in swarm.TaskSpec, d *schema.ResourceData) []interface{} { m := make(map[string]interface{}) if in.ContainerSpec != nil { m["container_spec"] = flattenContainerSpec(in.ContainerSpec) @@ -37,7 +37,22 @@ func flattenTaskSpec(in swarm.TaskSpec) []interface{} { m["runtime"] = in.Runtime } if len(in.Networks) > 0 { - m["networks"] = flattenTaskNetworks(in.Networks) + // We check which networks are set and need to retrieve the resource data + // therefore. See in the method 'createServiceTaskSpec' + v := d.Get("task_spec").([]interface{}) + // TODO mavogel: in the last cycle run v is empty + // so we check but then the import state fails + if len(v) > 0 { + rawTaskSpec := v[0].(map[string]interface{}) + if v, ok := rawTaskSpec["networks"]; ok && len(v.(*schema.Set).List()) > 0 { + log.Printf("[DEBUG] flatten networks with length: %d", rawTaskSpec["networks"]) + m["networks"] = flattenTaskNetworks(in.Networks) + } + if v, ok := rawTaskSpec["networks_advanced"]; ok && len(v.(*schema.Set).List()) > 0 { + log.Printf("[DEBUG] flatten networks_advanced with length: %d", rawTaskSpec["networks_advanced"]) + m["networks_advanced"] = flattenTaskNetworksAdvanced(in.Networks) + } + } } if in.LogDriver != nil { m["log_driver"] = flattenTaskLogDriver(in.LogDriver) @@ -513,6 +528,31 @@ func flattenTaskNetworks(in []swarm.NetworkAttachmentConfig) *schema.Set { return schema.NewSet(schema.HashString, out) } +func flattenTaskNetworksAdvanced(in []swarm.NetworkAttachmentConfig) *schema.Set { + out := make([]interface{}, len(in)) + for i, v := range in { + m := make(map[string]interface{}) + m["name"] = v.Target + m["driver_opts"] = stringSliceToSchemaSet(mapTypeMapValsToStringSlice(mapStringStringToMapStringInterface(v.DriverOpts))) + if len(v.Aliases) > 0 { + m["aliases"] = stringSliceToSchemaSet(v.Aliases) + } + out[i] = m + } + taskSpecResource := resourceDockerService().Schema["task_spec"].Elem.(*schema.Resource) + networksAdvancedResource := taskSpecResource.Schema["networks_advanced"].Elem.(*schema.Resource) + f := schema.HashResource(networksAdvancedResource) + return schema.NewSet(f, out) +} + +func stringSliceToSchemaSet(in []string) *schema.Set { + out := make([]interface{}, len(in)) + for i, v := range in { + out[i] = v + } + return schema.NewSet(schema.HashString, out) +} + func flattenTaskLogDriver(in *swarm.Driver) []interface{} { if in == nil { return make([]interface{}, 0) @@ -656,6 +696,13 @@ func createServiceTaskSpec(d *schema.ResourceData) (swarm.TaskSpec, error) { } taskSpec.Networks = networks } + if rawNetworksSpec, ok := rawTaskSpec["networks_advanced"]; ok { + networks, err := createServiceAdvancedNetworks(rawNetworksSpec) + if err != nil { + return taskSpec, err + } + taskSpec.Networks = networks + } if rawLogDriverSpec, ok := rawTaskSpec["log_driver"]; ok { logDriver, err := createLogDriver(rawLogDriverSpec) if err != nil { @@ -1071,7 +1118,7 @@ func createPlacement(v interface{}) (*swarm.Placement, error) { return &placement, nil } -// createServiceNetworks creates the networks the service will be attachted to +// createServiceNetworks creates the networks the service will be attachted to. Is deprecated func createServiceNetworks(v interface{}) ([]swarm.NetworkAttachmentConfig, error) { networks := []swarm.NetworkAttachmentConfig{} if len(v.(*schema.Set).List()) > 0 { @@ -1085,6 +1132,27 @@ func createServiceNetworks(v interface{}) ([]swarm.NetworkAttachmentConfig, erro return networks, nil } +// createServiceAdvancedNetworks creates the networks the service will be attachted to +func createServiceAdvancedNetworks(v interface{}) ([]swarm.NetworkAttachmentConfig, error) { + networks := []swarm.NetworkAttachmentConfig{} + if len(v.(*schema.Set).List()) > 0 { + for _, rawNetwork := range v.(*schema.Set).List() { + rawNetwork := rawNetwork.(map[string]interface{}) + networkID := rawNetwork["name"].(string) + networkAliases := stringSetToStringSlice(rawNetwork["aliases"].(*schema.Set)) + network := swarm.NetworkAttachmentConfig{ + Target: networkID, + Aliases: networkAliases, + } + if driverOpts, ok := rawNetwork["driver_opts"]; ok { + network.DriverOpts = stringSetToMapStringString(driverOpts.(*schema.Set)) + } + networks = append(networks, network) + } + } + return networks, nil +} + // createLogDriver creates the log driver for the service func createLogDriver(v interface{}) (*swarm.Driver, error) { logDriver := swarm.Driver{} diff --git a/internal/provider/resource_docker_service_test.go b/internal/provider/resource_docker_service_test.go index 8b8b953e..2525be77 100644 --- a/internal/provider/resource_docker_service_test.go +++ b/internal/provider/resource_docker_service_test.go @@ -501,7 +501,11 @@ func TestAccDockerService_fullSpec(t *testing.T) { } if len(s.Spec.TaskTemplate.Networks) != 1 || - s.Spec.TaskTemplate.Networks[0].Target == "" { + s.Spec.TaskTemplate.Networks[0].Target == "" || + len(s.Spec.TaskTemplate.Networks[0].Aliases) == 0 || + s.Spec.TaskTemplate.Networks[0].Aliases[0] != "tftest-foobar" || + s.Spec.TaskTemplate.Networks[0].DriverOpts == nil || + !mapEquals("foo", "bar", s.Spec.TaskTemplate.Networks[0].DriverOpts) { return fmt.Errorf("Service Spec.TaskTemplate.Networks is wrong: %s", s.Spec.TaskTemplate.Networks) } @@ -639,7 +643,8 @@ func TestAccDockerService_fullSpec(t *testing.T) { resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.placement.0.prefs.0", "spread=node.role.manager"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.placement.0.max_replicas", "2"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.force_update", "0"), - resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.networks.#", "1"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.networks.#", "0"), + resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.networks_advanced.#", "1"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.log_driver.0.name", "json-file"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.log_driver.0.options.max-file", "3"), resource.TestCheckResourceAttr("docker_service.foo", "task_spec.0.log_driver.0.options.max-size", "10m"), @@ -667,9 +672,10 @@ func TestAccDockerService_fullSpec(t *testing.T) { ), }, { - ResourceName: "docker_service.foo", - ImportState: true, - ImportStateVerify: true, + ResourceName: "docker_service.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"task_spec.0.networks_advanced"}, }, }, CheckDestroy: func(state *terraform.State) error { diff --git a/testdata/resources/docker_service/testAccDockerServiceFullSpec.tf b/testdata/resources/docker_service/testAccDockerServiceFullSpec.tf index b42f07a8..53df8582 100644 --- a/testdata/resources/docker_service/testAccDockerServiceFullSpec.tf +++ b/testdata/resources/docker_service/testAccDockerServiceFullSpec.tf @@ -175,7 +175,14 @@ resource "docker_service" "foo" { force_update = 0 runtime = "container" - networks = [docker_network.test_network.id] + + networks_advanced { + name = docker_network.test_network.id + aliases = ["tftest-foobar"] + driver_opts = [ + "foo=bar" + ] + } log_driver { name = "json-file"