diff --git a/CHANGELOG.md b/CHANGELOG.md index 81cabb8c..963752bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## 1.1.1 (Unreleased) +BUG FIXES +* Fixes no more 'force new resource' for container ports when +there are no changes. This was caused to the ascending order. See [GH-110] +for details and [[#115](https://github.com/terraform-providers/terraform-provider-docker/pull/115)] + DOCS * Corrects `networks_advanced` section [GH-109] * Corrects `tmpfs_options` section [GH-122] diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 815f3332..622238a9 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -1,15 +1,19 @@ package docker import ( + "log" + "github.com/hashicorp/terraform/helper/schema" ) func resourceDockerContainer() *schema.Resource { return &schema.Resource{ - Create: resourceDockerContainerCreate, - Read: resourceDockerContainerRead, - Update: resourceDockerContainerUpdate, - Delete: resourceDockerContainerDelete, + Create: resourceDockerContainerCreate, + Read: resourceDockerContainerRead, + Update: resourceDockerContainerUpdate, + Delete: resourceDockerContainerDelete, + MigrateState: resourceDockerContainerMigrateState, + SchemaVersion: 1, Schema: map[string]*schema.Schema{ "name": &schema.Schema{ @@ -259,6 +263,7 @@ func resourceDockerContainer() *schema.Resource { }, }, }, + DiffSuppressFunc: suppressIfPortsDidNotChangeForMigrationV0ToV1(), }, "host": &schema.Schema{ @@ -601,3 +606,49 @@ func resourceDockerContainer() *schema.Resource { }, } } + +func suppressIfPortsDidNotChangeForMigrationV0ToV1() schema.SchemaDiffSuppressFunc { + return func(k, old, new string, d *schema.ResourceData) bool { + portsOldRaw, portsNewRaw := d.GetChange("ports") + portsOld := portsOldRaw.([]interface{}) + portsNew := portsNewRaw.([]interface{}) + if len(portsOld) != len(portsNew) { + log.Printf("[DEBUG] suppress diff ports: old and new don't have the same length") + return false + } + log.Printf("[DEBUG] suppress diff ports: old and new have same length") + + for _, portOld := range portsOld { + portOldMapped := portOld.(map[string]interface{}) + oldInternalPort := portOldMapped["internal"] + portFound := false + for _, portNew := range portsNew { + portNewMapped := portNew.(map[string]interface{}) + newInternalPort := portNewMapped["internal"] + // port is still there in new + if newInternalPort == oldInternalPort { + log.Printf("[DEBUG] suppress diff ports: comparing port '%v'", oldInternalPort) + portFound = true + if portNewMapped["external"] != portOldMapped["external"] { + log.Printf("[DEBUG] suppress diff ports: 'external' changed for '%v'", oldInternalPort) + return false + } + if portNewMapped["ip"] != portOldMapped["ip"] { + log.Printf("[DEBUG] suppress diff ports: 'ip' changed for '%v'", oldInternalPort) + return false + } + if portNewMapped["protocol"] != portOldMapped["protocol"] { + log.Printf("[DEBUG] suppress diff ports: 'protocol' changed for '%v'", oldInternalPort) + return false + } + } + } + // port was deleted or exchanges in new + if !portFound { + log.Printf("[DEBUG] suppress diff ports: port was deleted '%v'", oldInternalPort) + return false + } + } + return true + } +} diff --git a/docker/resource_docker_container_migrate.go b/docker/resource_docker_container_migrate.go new file mode 100644 index 00000000..f60d424e --- /dev/null +++ b/docker/resource_docker_container_migrate.go @@ -0,0 +1,115 @@ +package docker + +import ( + "fmt" + "log" + "sort" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func resourceDockerContainerMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found Docker Container State v0; migrating to v1") + return migrateDockerContainerMigrateStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func migrateDockerContainerMigrateStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] Docker Container Attributes before Migration: %#v", is.Attributes) + + err := updateV0ToV1PortsOrder(is, meta) + + log.Printf("[DEBUG] Docker Container Attributes after State Migration: %#v", is.Attributes) + + return is, err +} + +type mappedPort struct { + internal int + external int + ip string + protocol string +} + +type byPort []mappedPort + +func (s byPort) Len() int { + return len(s) +} +func (s byPort) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s byPort) Less(i, j int) bool { + return s[i].internal < s[j].internal +} + +func updateV0ToV1PortsOrder(is *terraform.InstanceState, meta interface{}) error { + reader := &schema.MapFieldReader{ + Schema: resourceDockerContainer().Schema, + Map: schema.BasicMapReader(is.Attributes), + } + + writer := &schema.MapFieldWriter{ + Schema: resourceDockerContainer().Schema, + } + + result, err := reader.ReadField([]string{"ports"}) + if err != nil { + return err + } + + if result.Value == nil { + return nil + } + + // map the ports into a struct, so they can be sorted easily + portsMapped := make([]mappedPort, 0) + portsRaw := result.Value.([]interface{}) + for _, portRaw := range portsRaw { + if portRaw == nil { + continue + } + portTyped := portRaw.(map[string]interface{}) + portMapped := mappedPort{ + internal: portTyped["internal"].(int), + external: portTyped["external"].(int), + ip: portTyped["ip"].(string), + protocol: portTyped["protocol"].(string), + } + + portsMapped = append(portsMapped, portMapped) + } + sort.Sort(byPort(portsMapped)) + + // map the sorted ports to an output structure tf can write + outputPorts := make([]interface{}, 0) + for _, mappedPort := range portsMapped { + outputPort := make(map[string]interface{}, 0) + outputPort["internal"] = mappedPort.internal + outputPort["external"] = mappedPort.external + outputPort["ip"] = mappedPort.ip + outputPort["protocol"] = mappedPort.protocol + outputPorts = append(outputPorts, outputPort) + } + + // store them back to state + if err := writer.WriteField([]string{"ports"}, outputPorts); err != nil { + return err + } + for k, v := range writer.Map() { + is.Attributes[k] = v + } + + return nil +}