From 5a40076c9502e54e6e934525614cdc07f6364991 Mon Sep 17 00:00:00 2001 From: Manuel Vogel Date: Mon, 29 Jan 2018 12:01:46 +0100 Subject: [PATCH] Feat/swarm 2 refactorings (#38) * Removed id attribute of network resource. * Extracted common validators. Removed custom hash functions. --- docker/resource_docker_container.go | 176 ++++------------------------ docker/resource_docker_network.go | 62 +++++----- docker/resource_docker_volume.go | 21 +++- docker/validators.go | 95 +++++++++++++++ docker/validators_test.go | 91 ++++++++++++++ 5 files changed, 257 insertions(+), 188 deletions(-) create mode 100644 docker/validators.go create mode 100644 docker/validators_test.go diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index aa47c951..c50aa92b 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -1,12 +1,10 @@ package docker import ( - "bytes" "fmt" "regexp" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -118,18 +116,11 @@ func resourceDockerContainer() *schema.Resource { }, "restart": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "no", - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(string) - if !regexp.MustCompile(`^(no|on-failure|always|unless-stopped)$`).MatchString(value) { - es = append(es, fmt.Errorf( - "%q must be one of \"no\", \"on-failure\", \"always\" or \"unless-stopped\"", k)) - } - return - }, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "no", + ValidateFunc: validateStringMatchesPattern(`^(no|on-failure|always|unless-stopped)$`), }, "max_retry_count": &schema.Schema{ @@ -162,7 +153,6 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - Set: resourceDockerCapabilitiesHash, }, "volumes": &schema.Schema{ @@ -203,7 +193,6 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - Set: resourceDockerVolumesHash, }, "ports": &schema.Schema{ @@ -238,7 +227,6 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - Set: resourceDockerPortsHash, }, "host": &schema.Schema{ @@ -260,7 +248,6 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - Set: resourceDockerHostsHash, }, "env": &schema.Schema{ @@ -317,57 +304,32 @@ func resourceDockerContainer() *schema.Resource { }, "memory": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(int) - if value < 0 { - es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k)) - } - return - }, + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(0), }, "memory_swap": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(int) - if value < -1 { - es = append(es, fmt.Errorf("%q must be greater than or equal to -1", k)) - } - return - }, + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(-1), }, "cpu_shares": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(int) - if value < 0 { - es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k)) - } - return - }, + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validateIntegerGeqThan(0), }, "log_driver": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "json-file", - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(string) - if !regexp.MustCompile(`^(json-file|syslog|journald|gelf|fluentd|awslogs)$`).MatchString(value) { - es = append(es, fmt.Errorf( - "%q must be one of \"json-file\", \"syslog\", \"journald\", \"gelf\", \"fluentd\", or \"awslogs\"", k)) - } - return - }, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "json-file", + ValidateFunc: validateStringMatchesPattern(`^(json-file|syslog|journald|gelf|fluentd|awslogs)$`), }, "log_opts": &schema.Schema{ @@ -418,105 +380,11 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - Set: resourceDockerUploadHash, }, }, } } -func resourceDockerCapabilitiesHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - - if v, ok := m["add"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v)) - } - - if v, ok := m["remove"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v)) - } - - return hashcode.String(buf.String()) -} - -func resourceDockerPortsHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - - buf.WriteString(fmt.Sprintf("%v-", m["internal"].(int))) - - if v, ok := m["external"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(int))) - } - - if v, ok := m["ip"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["protocol"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - return hashcode.String(buf.String()) -} - -func resourceDockerHostsHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - - if v, ok := m["ip"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["host"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - return hashcode.String(buf.String()) -} - -func resourceDockerVolumesHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - - if v, ok := m["from_container"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["container_path"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["host_path"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["volume_name"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["read_only"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(bool))) - } - - return hashcode.String(buf.String()) -} - -func resourceDockerUploadHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - - if v, ok := m["content"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - if v, ok := m["file"]; ok { - buf.WriteString(fmt.Sprintf("%v-", v.(string))) - } - - return hashcode.String(buf.String()) -} - func validateDockerContainerPath(v interface{}, k string) (ws []string, errors []error) { value := v.(string) diff --git a/docker/resource_docker_network.go b/docker/resource_docker_network.go index 1aa6f8c1..0978f20c 100644 --- a/docker/resource_docker_network.go +++ b/docker/resource_docker_network.go @@ -59,8 +59,34 @@ func resourceDockerNetwork() *schema.Resource { Type: schema.TypeSet, Optional: true, ForceNew: true, - Elem: getIpamConfigElem(), - Set: resourceDockerIpamConfigHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ip_range": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "gateway": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "aux_address": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + }, + Set: resourceDockerIpamConfigHash, }, "scope": &schema.Schema{ @@ -71,36 +97,6 @@ func resourceDockerNetwork() *schema.Resource { } } -func getIpamConfigElem() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "subnet": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "ip_range": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "gateway": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "aux_address": &schema.Schema{ - Type: schema.TypeMap, - Optional: true, - ForceNew: true, - }, - }, - } -} - func resourceDockerIpamConfigHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) @@ -122,7 +118,7 @@ func resourceDockerIpamConfigHash(v interface{}) int { keys := make([]string, len(auxAddress)) i := 0 - for k, _ := range auxAddress { + for k := range auxAddress { keys[i] = k i++ } diff --git a/docker/resource_docker_volume.go b/docker/resource_docker_volume.go index eb52202e..2b88204d 100644 --- a/docker/resource_docker_volume.go +++ b/docker/resource_docker_volume.go @@ -2,6 +2,8 @@ package docker import ( "fmt" + "log" + "time" dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/schema" @@ -94,7 +96,24 @@ func resourceDockerVolumeDelete(d *schema.ResourceData, meta interface{}) error client := meta.(*ProviderConfig).DockerClient if err := client.RemoveVolume(d.Id()); err != nil && err != dc.ErrNoSuchVolume { - return fmt.Errorf("Error deleting volume %s: %s", d.Id(), err) + if err == dc.ErrVolumeInUse { + loops := 50 + sleepTime := 1000 * time.Millisecond + for i := loops; i > 0; i-- { + if err = client.RemoveVolume(d.Id()); err != nil { + log.Printf("[INFO] Volume remove loop: %d of %d due to error: %s", loops-i+1, loops, err) + if err == dc.ErrVolumeInUse { + time.Sleep(sleepTime) + continue + } + if err == dc.ErrNoSuchVolume { + break // it's removed + } + // if it's not in use any more (so it's deleted successfully) and another error occurred + return fmt.Errorf("Error deleting volume %s: %s", d.Id(), err) + } + } + } } d.SetId("") diff --git a/docker/validators.go b/docker/validators.go new file mode 100644 index 00000000..447f5bb4 --- /dev/null +++ b/docker/validators.go @@ -0,0 +1,95 @@ +package docker + +import ( + "encoding/base64" + "fmt" + "regexp" + "time" + + "github.com/hashicorp/terraform/helper/schema" +) + +func validateIntegerInRange(min, max int) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < min { + errors = append(errors, fmt.Errorf( + "%q cannot be lower than %d: %d", k, min, value)) + } + if value > max { + errors = append(errors, fmt.Errorf( + "%q cannot be higher than %d: %d", k, max, value)) + } + return + } +} + +func validateIntegerGeqThan(threshold int) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < threshold { + errors = append(errors, fmt.Errorf( + "%q cannot be lower than %q", k, threshold)) + } + return + } +} + +func validateFloatRatio() schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(float64) + if value < 0.0 || value > 1.0 { + errors = append(errors, fmt.Errorf( + "%q has to be between 0.0 and 1.0", k)) + } + return + } +} + +func validateDurationGeq0() schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + dur, err := time.ParseDuration(value) + if err != nil { + errors = append(errors, fmt.Errorf( + "%q is not a valid duration", k)) + } + if dur < 0 { + errors = append(errors, fmt.Errorf( + "duration must not be negative")) + } + return + } +} + +func validateStringMatchesPattern(pattern string) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + compiledRegex, err := regexp.Compile(pattern) + if err != nil { + errors = append(errors, fmt.Errorf( + "%q regex does not compile", pattern)) + return + } + + value := v.(string) + if !compiledRegex.MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't match the pattern (%q): %q", + k, pattern, value)) + } + + return + } +} + +func validateStringIsBase64Encoded() schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if _, err := base64.StdEncoding.DecodeString(value); err != nil { + errors = append(errors, fmt.Errorf( + "%q is not base64 decodeable", k)) + } + + return + } +} diff --git a/docker/validators_test.go b/docker/validators_test.go new file mode 100644 index 00000000..75725847 --- /dev/null +++ b/docker/validators_test.go @@ -0,0 +1,91 @@ +package docker + +import "testing" + +func TestValidateIntegerInRange(t *testing.T) { + validIntegers := []int{-259, 0, 1, 5, 999} + min := -259 + max := 999 + for _, v := range validIntegers { + _, errors := validateIntegerInRange(min, max)(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be an integer in range (%d, %d): %q", v, min, max, errors) + } + } + + invalidIntegers := []int{-260, -99999, 1000, 25678} + for _, v := range invalidIntegers { + _, errors := validateIntegerInRange(min, max)(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an integer outside range (%d, %d)", v, min, max) + } + } +} + +func TestValidateIntegerGeqThan0(t *testing.T) { + v := 1 + if _, error := validateIntegerGeqThan(0)(v, "name"); error != nil { + t.Fatalf("%q should be an integer greater than 0", v) + } + + v = -4 + if _, error := validateIntegerGeqThan(0)(v, "name"); error == nil { + t.Fatalf("%q should be an invalid integer smaller than 0", v) + } +} + +func TestValidateFloatRatio(t *testing.T) { + v := 0.9 + if _, error := validateFloatRatio()(v, "name"); error != nil { + t.Fatalf("%v should be a float between 0.0 and 1.0", v) + } + + v = -4.5 + if _, error := validateFloatRatio()(v, "name"); error == nil { + t.Fatalf("%v should be an invalid float smaller than 0.0", v) + } + + v = 1.1 + if _, error := validateFloatRatio()(v, "name"); error == nil { + t.Fatalf("%v should be an invalid float greater than 1.0", v) + } +} +func TestValidateDurationGeq0(t *testing.T) { + v := "1ms" + if _, error := validateDurationGeq0()(v, "name"); error != nil { + t.Fatalf("%v should be a valid durarion", v) + } + + v = "-2h" + if _, error := validateDurationGeq0()(v, "name"); error == nil { + t.Fatalf("%v should be an invalid duration smaller than 0", v) + } +} + +func TestValidateStringMatchesPattern(t *testing.T) { + pattern := `^(pause|continue-mate|break)$` + v := "pause" + if _, error := validateStringMatchesPattern(pattern)(v, "name"); error != nil { + t.Fatalf("%q should match the pattern", v) + } + v = "doesnotmatch" + if _, error := validateStringMatchesPattern(pattern)(v, "name"); error == nil { + t.Fatalf("%q should not match the pattern", v) + } + v = "continue-mate" + if _, error := validateStringMatchesPattern(pattern)(v, "name"); error != nil { + t.Fatalf("%q should match the pattern", v) + } +} + +func TestValidateStringShouldBeBase64Encoded(t *testing.T) { + v := `YmtzbGRrc2xka3NkMjM4MQ==` + if _, error := validateStringIsBase64Encoded()(v, "name"); error != nil { + t.Fatalf("%q should be base64 decodeable", v) + } + + v = `%&df#3NkMjM4MQ==` + if _, error := validateStringIsBase64Encoded()(v, "name"); error == nil { + t.Fatalf("%q should NOT be base64 decodeable", v) + } +}