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 <Junkern@users.noreply.github.com>
Co-authored-by: Martin Wentzel <martin.wentzel@kreuzwerker.de>
This commit is contained in:
Manuel Vogel 2023-01-04 14:03:13 +01:00 committed by GitHub
parent 5ffe0f3628
commit a5332be18d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 16 deletions

View file

@ -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
<a id="nestedblock--task_spec--networks_advanced"></a>
### 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`
<a id="nestedblock--task_spec--placement"></a>
### Nested Schema for `task_spec.placement`

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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{}

View file

@ -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 {

View file

@ -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"