diff --git a/GNUmakefile b/GNUmakefile index c6dc27e..71c8d6a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -23,6 +23,9 @@ test: testacc: TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m +testacc-no-cache: + TF_ACC=1 GOCACHE=off go test $(TEST) -v $(TESTARGS) -timeout 120m + vet: @echo "go vet ." @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ diff --git a/README.md b/README.md index bb6d492..263b644 100644 --- a/README.md +++ b/README.md @@ -86,22 +86,47 @@ resource "ghost_app" "test" { environment_infos = { instance_profile = "iam.ec2.demo" key_name = "ghost-demo" - root_block_device = {} - optional_volumes = [] + root_block_device = { + name = "testblockdevice" + size = 20 + } + optional_volumes = [{ + device_name = "/dev/xvdd" + volume_type = "gp2" + volume_size = 20 + }] subnet_ids = ["subnet-a7e849fe"] - security_groups = ["sg-6814f60c"] + security_groups = ["sg-6814f60c", "sg-2414f60c"] + instance_tags = [{ + tag_name = "Name" + tag_value = "wordpress" + }, + { + tag_name = "Type" + tag_value = "front" + }] } autoscale = { - name = "" + name = "autoscale" + min = 1 + max = 3 } modules = [{ - name = "symfony2" - pre_deploy = "ZXhpdCAx" + name = "wordpress" + pre_deploy = "" path = "/var/www" scope = "code" git_repo = "https://github.com/KnpLabs/KnpIpsum.git" + }, + { + name = "wordpress2" + pre_deploy = "ZXhpdCAx" + post_deploy = "ZXhpdCAx" + path = "/var/www" + scope = "code" + git_repo = "https://github.com/KnpLabs/KnpIpsum.git" }] features = [{ @@ -112,5 +137,15 @@ resource "ghost_app" "test" { version = "2.2" name = "apache2" }] + + lifecycle_hooks = { + pre_buildimage = "#!/usr/bin/env bash" + post_buildimage = "#!/usr/bin/env bash" + } + + environment_variables = [{ + key = "myvar" + value = "myvalue" + }] } ``` diff --git a/examples/main.tf b/examples/main.tf index 5e265eb..fa0d301 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -26,22 +26,47 @@ resource "ghost_app" "test" { environment_infos = { instance_profile = "iam.ec2.demo" key_name = "ghost-demo" - root_block_device = {} - optional_volumes = [] + root_block_device = { + name = "testblockdevice" + size = 20 + } + optional_volumes = [{ + device_name = "/dev/xvdd" + volume_type = "gp2" + volume_size = 20 + }] subnet_ids = ["subnet-a7e849fe"] - security_groups = ["sg-6814f60c"] + security_groups = ["sg-6814f60c", "sg-2414f60c"] + instance_tags = [{ + tag_name = "Name" + tag_value = "wordpress" + }, + { + tag_name = "Type" + tag_value = "front" + }] } autoscale = { - name = "" + name = "autoscale" + min = 1 + max = 3 } modules = [{ - name = "symfony2" - pre_deploy = "ZXhpdCAx" + name = "wordpress" + pre_deploy = "" path = "/var/www" scope = "code" git_repo = "https://github.com/KnpLabs/KnpIpsum.git" + }, + { + name = "wordpress2" + pre_deploy = "ZXhpdCAx" + post_deploy = "ZXhpdCAx" + path = "/var/www" + scope = "code" + git_repo = "https://github.com/KnpLabs/KnpIpsum.git" }] features = [{ @@ -52,4 +77,14 @@ resource "ghost_app" "test" { version = "2.2" name = "apache2" }] + + lifecycle_hooks = { + pre_buildimage = "#!/usr/bin/env bash" + post_buildimage = "#!/usr/bin/env bash" + } + + environment_variables = [{ + key = "myvar" + value = "myvalue" + }] } diff --git a/ghost/helpers.go b/ghost/helpers.go new file mode 100644 index 0000000..ada0aab --- /dev/null +++ b/ghost/helpers.go @@ -0,0 +1,27 @@ +package ghost + +import ( + "encoding/base64" + "fmt" + "regexp" +) + +func StrToB64(data string) string { + return base64.StdEncoding.EncodeToString([]byte(data)) +} + +func B64ToStr(data string) string { + str, _ := base64.StdEncoding.DecodeString(data) + + return string(str) +} + +func MatchesRegexp(exp string) func(v interface{}, k string) (ws []string, errors []error) { + return func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(exp).MatchString(value) { + errors = append(errors, fmt.Errorf("%q must match %s", k, exp)) + } + return + } +} diff --git a/ghost/helpers_test.go b/ghost/helpers_test.go new file mode 100644 index 0000000..45bf8ee --- /dev/null +++ b/ghost/helpers_test.go @@ -0,0 +1,62 @@ +package ghost + +import ( + "reflect" + "testing" +) + +func TestStrToB64(t *testing.T) { + cases := []struct { + Input string + ExpectedOutput string + }{ + {"mystring", "bXlzdHJpbmc="}, + {"", ""}, + } + + for _, tc := range cases { + output := StrToB64(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from StrToB64.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestB64ToStr(t *testing.T) { + cases := []struct { + Input string + ExpectedOutput string + }{ + {"bXlzdHJpbmc=", "mystring"}, + {"", ""}, + {"-1", ""}, + {"()", ""}, + } + + for _, tc := range cases { + output := B64ToStr(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from B64ToStr.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestMatchesRegexp(t *testing.T) { + cases := []struct { + Function func(v interface{}, k string) (ws []string, errors []error) + Value string + Valid bool + }{ + {MatchesRegexp(`^[a-zA-Z0-9]*$`), "thisIsAPositiveTest", true}, + {MatchesRegexp(`^[a-zA-Z0-9]*$`), "thisIsANegativeTest-", false}, + } + + for _, tc := range cases { + _, err := tc.Function(tc.Value, tc.Value) + if (tc.Valid && (err != nil)) || (!tc.Valid && (err == nil)) { + t.Fatalf("Unexpected output from MatchesRegexp: %v", err) + } + } +} diff --git a/ghost/resource_ghost_app.go b/ghost/resource_ghost_app.go index 0d6d97e..4df47aa 100644 --- a/ghost/resource_ghost_app.go +++ b/ghost/resource_ghost_app.go @@ -5,6 +5,7 @@ import ( "cloud-deploy.io/go-st" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceGhostApp() *schema.Resource { @@ -16,24 +17,28 @@ func resourceGhostApp() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9_.+-]*$`), }, "env": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-z0-9\-\_]*$`), }, "role": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-z0-9\-\_]*$`), }, "region": { Type: schema.TypeString, Optional: true, }, "vpc_id": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^vpc-[a-z0-9]*$`), }, "instance_type": { Type: schema.TypeString, @@ -46,7 +51,7 @@ func resourceGhostApp() *schema.Resource { }, "autoscale": { Type: schema.TypeList, - Required: true, + Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -60,12 +65,14 @@ func resourceGhostApp() *schema.Resource { Default: false, }, "min": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), }, "max": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), }, }, }, @@ -102,21 +109,24 @@ func resourceGhostApp() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ssh_username": { - Type: schema.TypeString, - Optional: true, - Default: "admin", + Type: schema.TypeString, + Optional: true, + Default: "admin", + ValidateFunc: MatchesRegexp(`^[a-z\_][a-z0-9\_\-]{0,30}$`), }, "source_ami": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^ami-[a-z0-9]*$`), }, "ami_name": { Type: schema.TypeString, Optional: true, }, "subnet_id": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^subnet-[a-z0-9]*$`), }, }, }, @@ -128,12 +138,14 @@ func resourceGhostApp() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instance_profile": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9\+\=\,\.\@\-\_]{1,128}$`), }, "key_name": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: MatchesRegexp(`^[\p{Latin}\p{P}]{1,255}$`), }, "public_ip_address": { Type: schema.TypeBool, @@ -141,30 +153,35 @@ func resourceGhostApp() *schema.Resource { Default: true, }, "root_block_device": { - Type: schema.TypeMap, + Type: schema.TypeList, Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "size": { - Type: schema.TypeInt, - Optional: true, - Default: 20, + Type: schema.TypeInt, + Optional: true, + Default: 20, + ValidateFunc: validation.IntAtLeast(20), }, "name": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: MatchesRegexp(`^$|^(/[a-z0-9]+/)?[a-z0-9]+$`), }, }, }, }, "security_groups": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: MatchesRegexp(`^sg-[a-z0-9]*$`), + }, Optional: true, - Set: schema.HashString, }, "instance_tags": { - Type: schema.TypeMap, + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -180,8 +197,11 @@ func resourceGhostApp() *schema.Resource { }, }, "subnet_ids": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: MatchesRegexp(`^subnet-[a-z0-9]*$`), + }, Optional: true, }, "optional_volumes": { @@ -190,12 +210,15 @@ func resourceGhostApp() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "device_name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^/dev/xvd[b-m]$`), }, "volume_type": { Type: schema.TypeString, Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "gp2", "io1", "standard", "st1", "sc1"}, false), }, "volume_size": { Type: schema.TypeInt, @@ -221,8 +244,9 @@ func resourceGhostApp() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "key": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z_]+[a-zA-Z0-9_]*$`), }, "value": { Type: schema.TypeString, @@ -232,8 +256,11 @@ func resourceGhostApp() *schema.Resource { }, }, "log_notifications": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`), + }, Optional: true, }, "blue_green": { @@ -246,8 +273,9 @@ func resourceGhostApp() *schema.Resource { Optional: true, }, "color": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"blue", "green"}, false), }, "is_online": { Type: schema.TypeBool, @@ -283,20 +311,19 @@ func resourceGhostApp() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9\.\-\_]*$`), }, "version": { - Type: schema.TypeString, - Optional: true, - }, - "parameters": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9\.\-\_\/:~\+=\,]*$`), }, "provisioner": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9]*$`), }, }, }, @@ -328,38 +355,39 @@ func resourceGhostApp() *schema.Resource { }, "modules": { Type: schema.TypeList, - Optional: true, + Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^[a-zA-Z0-9\.\-\_]*$`), }, "git_repo": { Type: schema.TypeString, Required: true, }, "path": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: MatchesRegexp(`^(/[a-zA-Z0-9\.\-\_]+)+$`), }, "scope": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"system", "code"}, false), }, "uid": { - Type: schema.TypeInt, - Optional: true, - Default: 0, + Type: schema.TypeInt, + Optional: true, + Default: 0, + ValidateFunc: validation.IntAtLeast(0), }, "gid": { - Type: schema.TypeInt, - Optional: true, - Default: 0, - }, - "initialized": { - Type: schema.TypeBool, - Optional: true, + Type: schema.TypeInt, + Optional: true, + Default: 0, + ValidateFunc: validation.IntAtLeast(0), }, "build_pack": { Type: schema.TypeString, @@ -387,6 +415,7 @@ func resourceGhostApp() *schema.Resource { "safe_deployment": { Type: schema.TypeList, Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ha_backend": { @@ -406,12 +435,14 @@ func resourceGhostApp() *schema.Resource { Optional: true, }, "wait_before_deploy": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), }, "wait_after_deploy": { - Type: schema.TypeInt, - Optional: true, + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), }, }, }, @@ -423,19 +454,19 @@ func resourceGhostApp() *schema.Resource { func resourceGhostAppCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ghost.Client) - name := d.Get("name").(string) - d.SetId(name) log.Printf("[INFO] Creating Ghost app %s", d.Get("name").(string)) + app := expandGhostApp(d) - log.Printf("[INFO] Testing Ghost client get all apps") - apps, err := client.GetApps() + eveMetadata, err := client.CreateApp(app) if err == nil { - log.Println("All apps retrieved: ", apps) + log.Println("[INFO] App created: " + eveMetadata.ID) } else { - log.Printf("error: %v", err) + log.Fatalf("[ERROR] error: %v", err) } - return nil + d.SetId(eveMetadata.ID) + + return resourceGhostAppRead(d, meta) } func resourceGhostAppRead(d *schema.ResourceData, meta interface{}) error { @@ -449,7 +480,7 @@ func resourceGhostAppRead(d *schema.ResourceData, meta interface{}) error { func resourceGhostAppUpdate(d *schema.ResourceData, meta interface{}) error { //client := meta.(*ghost.Client) log.Printf("[INFO] Updating Ghost app %s", d.Get("name").(string)) - return nil + return resourceGhostAppRead(d, meta) } func resourceGhostAppDelete(d *schema.ResourceData, meta interface{}) error { @@ -458,3 +489,215 @@ func resourceGhostAppDelete(d *schema.ResourceData, meta interface{}) error { d.SetId("") return nil } + +// Get app from TF configuration +func expandGhostApp(d *schema.ResourceData) ghost.App { + app := ghost.App{ + Name: d.Get("name").(string), + Env: d.Get("env").(string), + Role: d.Get("role").(string), + Region: d.Get("region").(string), + InstanceType: d.Get("instance_type").(string), + VpcID: d.Get("vpc_id").(string), + InstanceMonitoring: d.Get("instance_monitoring").(bool), + + Modules: expandGhostAppModules(d.Get("modules").([]interface{})), + Features: expandGhostAppFeatures(d.Get("features").([]interface{})), + Autoscale: expandGhostAppAutoscale(d.Get("autoscale").([]interface{})), + BuildInfos: expandGhostAppBuildInfos(d.Get("build_infos").([]interface{})), + EnvironmentInfos: expandGhostAppEnvironmentInfos(d.Get("environment_infos").([]interface{})), + LifecycleHooks: expandGhostAppLifecycleHooks(d.Get("lifecycle_hooks").([]interface{})), + LogNotifications: expandGhostAppStringList(d.Get("log_notifications").([]interface{})), + EnvironmentVariables: expandGhostAppEnvironmentVariables(d.Get("environment_variables").([]interface{})), + } + + return app +} + +// Get modules from TF configuration +func expandGhostAppModules(d []interface{}) *[]ghost.Module { + modules := &[]ghost.Module{} + + // Add each module to modules list + for _, config := range d { + data := config.(map[string]interface{}) + module := ghost.Module{ + Name: data["name"].(string), + GitRepo: data["git_repo"].(string), + Scope: data["scope"].(string), + Path: data["path"].(string), + BuildPack: StrToB64(data["build_pack"].(string)), + PreDeploy: StrToB64(data["pre_deploy"].(string)), + PostDeploy: StrToB64(data["post_deploy"].(string)), + AfterAllDeploy: StrToB64(data["after_all_deploy"].(string)), + LastDeployment: data["last_deployment"].(string), + GID: data["gid"].(int), + UID: data["uid"].(int), + } + + *modules = append(*modules, module) + } + + return modules +} + +// Get environment variables from TF configuration +func expandGhostAppEnvironmentVariables(d []interface{}) *[]ghost.EnvironmentVariable { + environmentVariables := &[]ghost.EnvironmentVariable{} + + for _, config := range d { + data := config.(map[string]interface{}) + environmentVariable := ghost.EnvironmentVariable{ + Key: data["key"].(string), + Value: data["value"].(string), + } + + *environmentVariables = append(*environmentVariables, environmentVariable) + } + + return environmentVariables +} + +// Get autoscale from TF configuration +func expandGhostAppAutoscale(d []interface{}) *ghost.Autoscale { + if len(d) == 0 { + return nil + } + data := d[0].(map[string]interface{}) + + autoscale := &ghost.Autoscale{ + Name: data["name"].(string), + EnableMetrics: data["enable_metrics"].(bool), + Min: data["min"].(int), + Max: data["max"].(int), + } + + return autoscale +} + +// Get lifecycle_hooks from TF configuration +func expandGhostAppLifecycleHooks(d []interface{}) *ghost.LifecycleHooks { + if len(d) == 0 { + return nil + } + data := d[0].(map[string]interface{}) + + lifecycleHooks := &ghost.LifecycleHooks{ + PreBuildimage: StrToB64(data["pre_buildimage"].(string)), + PostBuildimage: StrToB64(data["post_buildimage"].(string)), + PreBootstrap: StrToB64(data["pre_bootstrap"].(string)), + PostBootstrap: StrToB64(data["post_bootstrap"].(string)), + } + + return lifecycleHooks +} + +// Get features from TF configuration +func expandGhostAppFeatures(d []interface{}) *[]ghost.Feature { + features := &[]ghost.Feature{} + + for _, config := range d { + data := config.(map[string]interface{}) + feature := ghost.Feature{ + Name: data["name"].(string), + Version: data["version"].(string), + Provisioner: data["provisioner"].(string), + } + + *features = append(*features, feature) + } + + return features +} + +// Get build_infos from TF configuration +func expandGhostAppBuildInfos(d []interface{}) *ghost.BuildInfos { + data := d[0].(map[string]interface{}) + + buildInfos := &ghost.BuildInfos{ + SshUsername: data["ssh_username"].(string), + SourceAmi: data["source_ami"].(string), + AmiName: data["ami_name"].(string), + SubnetID: data["subnet_id"].(string), + } + + return buildInfos +} + +// Get environment_infos from TF configuration +func expandGhostAppEnvironmentInfos(d []interface{}) *ghost.EnvironmentInfos { + data := d[0].(map[string]interface{}) + + environmentInfos := &ghost.EnvironmentInfos{ + InstanceProfile: data["instance_profile"].(string), + KeyName: data["key_name"].(string), + PublicIpAddress: data["public_ip_address"].(bool), + SecurityGroups: expandGhostAppStringList(data["security_groups"].([]interface{})), + SubnetIDs: expandGhostAppStringList(data["subnet_ids"].([]interface{})), + InstanceTags: expandGhostAppInstanceTags(data["instance_tags"].([]interface{})), + OptionalVolumes: expandGhostAppOptionalVolumes(data["optional_volumes"].([]interface{})), + RootBlockDevice: expandGhostAppRootBlockDevice(data["root_block_device"].([]interface{})), + } + + return environmentInfos +} + +func expandGhostAppRootBlockDevice(d []interface{}) *ghost.RootBlockDevice { + if len(d) == 0 { + return nil + } + + data := d[0].(map[string]interface{}) + + rootBlockDevice := &ghost.RootBlockDevice{ + Name: data["name"].(string), + Size: data["size"].(int), + } + + return rootBlockDevice +} + +func expandGhostAppOptionalVolumes(d []interface{}) *[]ghost.OptionalVolume { + optionalVolumes := &[]ghost.OptionalVolume{} + + for _, config := range d { + data := config.(map[string]interface{}) + optionalVolume := ghost.OptionalVolume{ + DeviceName: data["device_name"].(string), + VolumeType: data["volume_type"].(string), + VolumeSize: data["volume_size"].(int), + Iops: data["iops"].(int), + LaunchBlockDeviceMappings: data["launch_block_device_mappings"].(bool), + } + + *optionalVolumes = append(*optionalVolumes, optionalVolume) + } + + return optionalVolumes +} + +func expandGhostAppInstanceTags(d []interface{}) *[]ghost.InstanceTag { + instanceTags := &[]ghost.InstanceTag{} + + for _, config := range d { + data := config.(map[string]interface{}) + instanceTag := ghost.InstanceTag{ + TagName: data["tag_name"].(string), + TagValue: data["tag_value"].(string), + } + + *instanceTags = append(*instanceTags, instanceTag) + } + + return instanceTags +} + +func expandGhostAppStringList(d []interface{}) []string { + var stringList []string + + for _, str := range d { + stringList = append(stringList, str.(string)) + } + + return stringList +} diff --git a/ghost/resource_ghost_app_test.go b/ghost/resource_ghost_app_test.go index 258830e..3453a91 100644 --- a/ghost/resource_ghost_app_test.go +++ b/ghost/resource_ghost_app_test.go @@ -3,6 +3,7 @@ package ghost import ( "fmt" "log" + "reflect" "testing" "cloud-deploy.io/go-st" @@ -25,7 +26,6 @@ func TestAccGhostAppBasic(t *testing.T) { testAccCheckGhostAppExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", envName), resource.TestCheckResourceAttr(resourceName, "env", "dev"), - resource.TestCheckResourceAttr(resourceName, "env", "dev"), ), }, }, @@ -78,23 +78,48 @@ func testAccGhostAppConfig(name string) string { environment_infos = { instance_profile = "iam.ec2.demo" key_name = "ghost-demo" - root_block_device = {} - optional_volumes = [] + root_block_device = { + name = "testblockdevice" + size = 20 + } + optional_volumes = [{ + device_name = "/dev/xvdd" + volume_type = "gp2" + volume_size = 20 + }] subnet_ids = ["subnet-a7e849fe"] - security_groups = ["sg-6814f60c"] + security_groups = ["sg-6814f60c", "sg-2414f60c"] + instance_tags = [{ + tag_name = "Name" + tag_value = "wordpress" + }, + { + tag_name = "Type" + tag_value = "front" + }] } autoscale = { - name = "" + name = "autoscale" + min = 1 + max = 3 } modules = [{ - name = "symfony2" - pre_deploy = "ZXhpdCAx" + name = "wordpress" + pre_deploy = "" path = "/var/www" scope = "code" git_repo = "https://github.com/KnpLabs/KnpIpsum.git" - }] + }, + { + name = "wordpress2" + pre_deploy = "ZXhpdCAx" + post_deploy = "ZXhpdCAx" + path = "/var/www-test.test" + scope = "code" + git_repo = "https://github.com/KnpLabs/KnpIpsum.git" + }] features = [{ version = "5.4" @@ -104,6 +129,433 @@ func testAccGhostAppConfig(name string) string { version = "2.2" name = "apache2" }] + + lifecycle_hooks = { + pre_buildimage = "#!/usr/bin/env bash" + post_buildimage = "#!/usr/bin/env bash" + } + + environment_variables = [{ + key = "myvar" + value = "myvalue" + }] } `, name) } + +var ( + app = ghost.App{ + Name: "app_name", + Env: "test", + Role: "web", + Region: "us-west-1", + InstanceType: "t2.micro", + VpcID: "vpc-123456", + InstanceMonitoring: false, + + Modules: &[]ghost.Module{{ + Name: "my_module", + GitRepo: "https://github.com/test/test.git", + Scope: "system", + Path: "/", + BuildPack: StrToB64("#!/usr/bin/env bash"), + PreDeploy: StrToB64("#!/usr/bin/env bash"), + }}, + Features: &[]ghost.Feature{{ + Name: "feature", + Version: "1.0", + Provisioner: "ansible", + }}, + Autoscale: &ghost.Autoscale{ + Name: "autoscale", + EnableMetrics: false, + Min: 0, + Max: 3, + }, + BuildInfos: &ghost.BuildInfos{ + SshUsername: "admin", + SourceAmi: "ami-1", + SubnetID: "subnet-1", + }, + EnvironmentInfos: &ghost.EnvironmentInfos{ + InstanceProfile: "profile", + KeyName: "key", + PublicIpAddress: false, + SecurityGroups: []string{"sg-1", "sg-2"}, + SubnetIDs: []string{"subnet-1", "subnet-2"}, + InstanceTags: &[]ghost.InstanceTag{{ + TagName: "name", + TagValue: "val", + }}, + OptionalVolumes: &[]ghost.OptionalVolume{{ + DeviceName: "my_device", + VolumeType: "gp2", + VolumeSize: 20, + Iops: 3000, + }}, + RootBlockDevice: &ghost.RootBlockDevice{ + Name: "rootblock", + Size: 20, + }, + }, + LifecycleHooks: &ghost.LifecycleHooks{ + PreBuildimage: StrToB64("#!/usr/bin/env bash"), + PostBuildimage: StrToB64("#!/usr/bin/env bash"), + }, + LogNotifications: []string{"log_not@email.com"}, + EnvironmentVariables: &[]ghost.EnvironmentVariable{{ + Key: "env_var_key", + Value: "env_var_value", + }}, + } +) + +// Expanders Unit Tests +func TestExpandGhostAppStringList(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput []string + }{ + { + []interface{}{ + "1", "2", "3", + }, + []string{ + "1", "2", "3", + }, + }, + { + nil, + nil, + }, + } + + for _, tc := range cases { + output := expandGhostAppStringList(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppInstanceTags(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *[]ghost.InstanceTag + }{ + { + []interface{}{ + map[string]interface{}{ + "tag_name": "name", + "tag_value": "val", + }, + }, + app.EnvironmentInfos.InstanceTags, + }, + { + nil, + &[]ghost.InstanceTag{}, + }, + } + + for _, tc := range cases { + output := expandGhostAppInstanceTags(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppOptionalVolume(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *[]ghost.OptionalVolume + }{ + { + []interface{}{ + map[string]interface{}{ + "device_name": "my_device", + "volume_type": "gp2", + "volume_size": 20, + "iops": 3000, + "launch_block_device_mappings": false, + }, + }, + app.EnvironmentInfos.OptionalVolumes, + }, + { + nil, + &[]ghost.OptionalVolume{}, + }, + } + + for _, tc := range cases { + output := expandGhostAppOptionalVolumes(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppRootBlockDevice(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *ghost.RootBlockDevice + }{ + { + []interface{}{ + map[string]interface{}{ + "name": "rootblock", + "size": 20, + }, + }, + app.EnvironmentInfos.RootBlockDevice, + }, + { + nil, + nil, + }, + } + + for _, tc := range cases { + output := expandGhostAppRootBlockDevice(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppEnvironmentInfos(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *ghost.EnvironmentInfos + }{ + { + []interface{}{ + map[string]interface{}{ + "instance_profile": "profile", + "key_name": "key", + "public_ip_address": false, + "security_groups": []interface{}{"sg-1", "sg-2"}, + "subnet_ids": []interface{}{"subnet-1", "subnet-2"}, + "instance_tags": []interface{}{ + map[string]interface{}{ + "tag_name": "name", + "tag_value": "val", + }, + }, + "optional_volumes": []interface{}{ + map[string]interface{}{ + "device_name": "my_device", + "volume_type": "gp2", + "volume_size": 20, + "iops": 3000, + "launch_block_device_mappings": false, + }, + }, + "root_block_device": []interface{}{ + map[string]interface{}{ + "name": "rootblock", + "size": 20, + }, + }, + }, + }, + app.EnvironmentInfos, + }, + } + + for _, tc := range cases { + output := expandGhostAppEnvironmentInfos(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppBuildInfos(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *ghost.BuildInfos + }{ + { + []interface{}{ + map[string]interface{}{ + "ssh_username": "admin", + "source_ami": "ami-1", + "subnet_id": "subnet-1", + "ami_name": "", + }, + }, + app.BuildInfos, + }, + } + + for _, tc := range cases { + output := expandGhostAppBuildInfos(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppFeatures(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *[]ghost.Feature + }{ + { + []interface{}{ + map[string]interface{}{ + "name": "feature", + "version": "1.0", + "provisioner": "ansible", + }, + }, + app.Features, + }, + { + nil, + &[]ghost.Feature{}, + }, + } + + for _, tc := range cases { + output := expandGhostAppFeatures(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppLifecycleHooks(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *ghost.LifecycleHooks + }{ + { + []interface{}{ + map[string]interface{}{ + "pre_buildimage": "#!/usr/bin/env bash", + "post_buildimage": "#!/usr/bin/env bash", + "pre_bootstrap": "", + "post_bootstrap": "", + }, + }, + app.LifecycleHooks, + }, + { + nil, + nil, + }, + } + + for _, tc := range cases { + output := expandGhostAppLifecycleHooks(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppAutoscale(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *ghost.Autoscale + }{ + { + []interface{}{ + map[string]interface{}{ + "name": "autoscale", + "enable_metrics": false, + "min": 0, + "max": 3, + }, + }, + app.Autoscale, + }, + { + nil, + nil, + }, + } + + for _, tc := range cases { + output := expandGhostAppAutoscale(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppEnvironmentVariables(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *[]ghost.EnvironmentVariable + }{ + { + []interface{}{ + map[string]interface{}{ + "key": "env_var_key", + "value": "env_var_value", + }, + }, + app.EnvironmentVariables, + }, + { + nil, + &[]ghost.EnvironmentVariable{}, + }, + } + + for _, tc := range cases { + output := expandGhostAppEnvironmentVariables(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} + +func TestExpandGhostAppModules(t *testing.T) { + cases := []struct { + Input []interface{} + ExpectedOutput *[]ghost.Module + }{ + { + []interface{}{ + map[string]interface{}{ + "name": "my_module", + "git_repo": "https://github.com/test/test.git", + "path": "/", + "scope": "system", + "build_pack": "#!/usr/bin/env bash", + "pre_deploy": "#!/usr/bin/env bash", + "post_deploy": "", + "after_all_deploy": "", + "uid": 0, + "gid": 0, + "last_deployment": "", + }, + }, + app.Modules, + }, + } + + for _, tc := range cases { + output := expandGhostAppModules(tc.Input) + if !reflect.DeepEqual(output, tc.ExpectedOutput) { + t.Fatalf("Unexpected output from expander.\nExpected: %#v\nGiven: %#v", + tc.ExpectedOutput, output) + } + } +} diff --git a/vendor/cloud-deploy.io/go-st/spec.go b/vendor/cloud-deploy.io/go-st/spec.go index a014dc4..4c0755f 100644 --- a/vendor/cloud-deploy.io/go-st/spec.go +++ b/vendor/cloud-deploy.io/go-st/spec.go @@ -38,37 +38,49 @@ type EveCollectionMetadata struct { // Ghost App's build_infos struct type BuildInfos struct { - SourceAmi string `json:"source_ami"` - SshUsername string `json:"ssh_username"` - SubnetID string `json:"subnet_id"` + SourceAmi string `json:"source_ami"` + SshUsername string `json:"ssh_username"` + SubnetID string `json:"subnet_id"` + AmiName string `json:"ami_name,omitempty"` + ContainerImage string `json:"container_image,omitempty"` + SourceContainerImage string `json:"source_container_image,omitempty"` } // Ghost App's environment_infos structs type OptionalVolume struct { - InstanceProfile string `json:"device_name"` - KeyName string `json:"volume_type"` - VolumeSize int `json:"volume_size"` - IOPS int `json:"iops"` + DeviceName string `json:"device_name"` + VolumeType string `json:"volume_type"` + VolumeSize int `json:"volume_size"` + Iops int `json:"iops,omitempty"` + LaunchBlockDeviceMappings bool `json:"launch_block_device_mappings,omitempty"` } type RootBlockDevice struct { - Size int `json:"size"` - Name string `json:"name"` + Size int `json:"size,omitempty"` + Name string `json:"name,omitempty"` +} + +type InstanceTag struct { + TagName string `json:"tag_name"` + TagValue string `json:"tag_value"` } type EnvironmentInfos struct { - InstanceProfile string `json:"instance_profile"` - KeyName string `json:"key_name"` - OptionalVolumes []OptionalVolume `json:"optional_volumes"` - RootBlockDevice RootBlockDevice `json:"root_block_device"` - SecurityGroups []string `json:"security_groups"` - SubnetIDs []string `json:"subnet_ids"` + InstanceProfile string `json:"instance_profile,omitempty"` + KeyName string `json:"key_name,omitempty"` + SecurityGroups []string `json:"security_groups,omitempty"` + SubnetIDs []string `json:"subnet_ids,omitempty"` + OptionalVolumes *[]OptionalVolume `json:"optional_volumes,omitempty"` + RootBlockDevice *RootBlockDevice `json:"root_block_device,omitempty"` + PublicIpAddress bool `json:"public_ip_address"` + InstanceTags *[]InstanceTag `json:"instance_tags,omitempty"` } // Ghost App's feature struct type Feature struct { - Name string `json:"name"` - Version string `json:"version"` + Name string `json:"name"` + Version string `json:"version,omitempty"` + Provisioner string `json:"provisioner,omitempty"` } // Ghost App's module struct @@ -84,10 +96,45 @@ type Module struct { GID int `json:"gid"` // Scripts - BuildPack string `json:"build_pack"` - PreDeploy string `json:"pre_deploy"` - PostDeploy string `json:"post_deploy"` - AfterAllDeploy string `json:"after_all_deploy"` + BuildPack string `json:"build_pack,omitempty"` + PreDeploy string `json:"pre_deploy,omitempty"` + PostDeploy string `json:"post_deploy,omitempty"` + AfterAllDeploy string `json:"after_all_deploy,omitempty"` + LastDeployment string `json:"last_deployment,omitempty"` +} + +type LifecycleHooks struct { + PreBuildimage string `json:"pre_buildimage,omitempty"` + PostBuildimage string `json:"post_buildimage,omitempty"` + PreBootstrap string `json:"pre_bootstrap,omitempty"` + PostBootstrap string `json:"post_bootstrap,omitempty"` +} + +type EnvironmentVariable struct { + Key string `json:"var_key,omitempty"` + Value string `json:"var_value,omitempty"` +} + +type Autoscale struct { + Min int `json:"min"` + Max int `json:"max"` + EnableMetrics bool `json:"enable_metrics"` + Name string `json:"name,omitempty"` +} + +type SafeDeployment struct { + WaitBeforeDeploy int `json:"wait_before_deploy"` + WaitAfterDeploy int `json:"wait_after_deploy"` + LoadBalancerType string `json:"load_balancer_type,omitempty"` + AppTagValue string `json:"app_tag_value,omitempty"` + HaBackend string `json:"ha_backend,omitempty"` + ApiPort string `json:"api_port,omitempty"` +} + +type PendingChange struct { + Field string `json:"field,omitempty"` + Updated string `json:"updated,omitempty"` + User string `json:"user,omitempty"` } // Ghost App struct @@ -99,19 +146,30 @@ type App struct { Env string `json:"env"` Role string `json:"role"` - Region string `json:"region"` - InstanceType string `json:"instance_type"` - VpcID string `json:"vpc_id"` + Region string `json:"region"` + InstanceType string `json:"instance_type"` + InstanceMonitoring bool `json:"instance_monitoring"` + VpcID string `json:"vpc_id"` + + LifecycleHooks *LifecycleHooks `json:"lifecycle_hooks,omitempty"` + + LogNotifications []string `json:"log_notifications,omitempty"` + + BuildInfos *BuildInfos `json:"build_infos,omitempty"` + + EnvironmentInfos *EnvironmentInfos `json:"environment_infos,omitempty"` + + EnvironmentVariables *[]EnvironmentVariable `json:"env_vars,omitempty"` - LogNotifications []string `json:"log_notifications"` + Features *[]Feature `json:"features,omitempty"` - BuildInfos BuildInfos `json:"build_infos"` + Modules *[]Module `json:"modules"` - EnvironmentInfos EnvironmentInfos `json:"environment_infos"` + Autoscale *Autoscale `json:"autoscale,omitempty"` - Features []Feature `json:"features"` + SafeDeployment *SafeDeployment `json:"safe-deployment,omitempty"` - Modules []Module `json:"modules"` + PendingChanges *[]PendingChange `json:"pending_changes,omitempty"` } // Ghost Apps collection diff --git a/vendor/github.com/hashicorp/terraform/helper/structure/expand_json.go b/vendor/github.com/hashicorp/terraform/helper/structure/expand_json.go new file mode 100644 index 0000000..b3eb90f --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/helper/structure/expand_json.go @@ -0,0 +1,11 @@ +package structure + +import "encoding/json" + +func ExpandJsonFromString(jsonString string) (map[string]interface{}, error) { + var result map[string]interface{} + + err := json.Unmarshal([]byte(jsonString), &result) + + return result, err +} diff --git a/vendor/github.com/hashicorp/terraform/helper/structure/flatten_json.go b/vendor/github.com/hashicorp/terraform/helper/structure/flatten_json.go new file mode 100644 index 0000000..578ad2e --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/helper/structure/flatten_json.go @@ -0,0 +1,16 @@ +package structure + +import "encoding/json" + +func FlattenJsonToString(input map[string]interface{}) (string, error) { + if len(input) == 0 { + return "", nil + } + + result, err := json.Marshal(input) + if err != nil { + return "", err + } + + return string(result), nil +} diff --git a/vendor/github.com/hashicorp/terraform/helper/structure/normalize_json.go b/vendor/github.com/hashicorp/terraform/helper/structure/normalize_json.go new file mode 100644 index 0000000..3256b47 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/helper/structure/normalize_json.go @@ -0,0 +1,24 @@ +package structure + +import "encoding/json" + +// Takes a value containing JSON string and passes it through +// the JSON parser to normalize it, returns either a parsing +// error or normalized JSON string. +func NormalizeJsonString(jsonString interface{}) (string, error) { + var j interface{} + + if jsonString == nil || jsonString.(string) == "" { + return "", nil + } + + s := jsonString.(string) + + err := json.Unmarshal([]byte(s), &j) + if err != nil { + return s, err + } + + bytes, _ := json.Marshal(j) + return string(bytes[:]), nil +} diff --git a/vendor/github.com/hashicorp/terraform/helper/structure/suppress_json_diff.go b/vendor/github.com/hashicorp/terraform/helper/structure/suppress_json_diff.go new file mode 100644 index 0000000..46f794a --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/helper/structure/suppress_json_diff.go @@ -0,0 +1,21 @@ +package structure + +import ( + "reflect" + + "github.com/hashicorp/terraform/helper/schema" +) + +func SuppressJsonDiff(k, old, new string, d *schema.ResourceData) bool { + oldMap, err := ExpandJsonFromString(old) + if err != nil { + return false + } + + newMap, err := ExpandJsonFromString(new) + if err != nil { + return false + } + + return reflect.DeepEqual(oldMap, newMap) +} diff --git a/vendor/github.com/hashicorp/terraform/helper/validation/validation.go b/vendor/github.com/hashicorp/terraform/helper/validation/validation.go new file mode 100644 index 0000000..1fc3a6c --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/helper/validation/validation.go @@ -0,0 +1,222 @@ +package validation + +import ( + "fmt" + "net" + "reflect" + "regexp" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/structure" +) + +// IntBetween returns a SchemaValidateFunc which tests if the provided value +// is of type int and is between min and max (inclusive) +func IntBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) + return + } + + return + } +} + +// IntAtLeast returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at least min (inclusive) +func IntAtLeast(min int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v < min { + es = append(es, fmt.Errorf("expected %s to be at least (%d), got %d", k, min, v)) + return + } + + return + } +} + +// IntAtMost returns a SchemaValidateFunc which tests if the provided value +// is of type int and is at most max (inclusive) +func IntAtMost(max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(int) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v > max { + es = append(es, fmt.Errorf("expected %s to be at most (%d), got %d", k, max, v)) + return + } + + return + } +} + +// StringInSlice returns a SchemaValidateFunc which tests if the provided value +// is of type string and matches the value of an element in the valid slice +// will test with in lower case if ignoreCase is true +func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + for _, str := range valid { + if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { + return + } + } + + es = append(es, fmt.Errorf("expected %s to be one of %v, got %s", k, valid, v)) + return + } +} + +// StringLenBetween returns a SchemaValidateFunc which tests if the provided value +// is of type string and has length between min and max (inclusive) +func StringLenBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + if len(v) < min || len(v) > max { + es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, min, max, v)) + } + return + } +} + +// StringMatch returns a SchemaValidateFunc which tests if the provided value +// matches a given regexp. Optionally an error message can be provided to +// return something friendlier than "must match some globby regexp". +func StringMatch(r *regexp.Regexp, message string) schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + v, ok := i.(string) + if !ok { + return nil, []error{fmt.Errorf("expected type of %s to be string", k)} + } + + if ok := r.MatchString(v); !ok { + if message != "" { + return nil, []error{fmt.Errorf("invalid value for %s (%s)", k, message)} + + } + return nil, []error{fmt.Errorf("expected value of %s to match regular expression %q", k, r)} + } + return nil, nil + } +} + +// NoZeroValues is a SchemaValidateFunc which tests if the provided value is +// not a zero value. It's useful in situations where you want to catch +// explicit zero values on things like required fields during validation. +func NoZeroValues(i interface{}, k string) (s []string, es []error) { + if reflect.ValueOf(i).Interface() == reflect.Zero(reflect.TypeOf(i)).Interface() { + switch reflect.TypeOf(i).Kind() { + case reflect.String: + es = append(es, fmt.Errorf("%s must not be empty", k)) + case reflect.Int, reflect.Float64: + es = append(es, fmt.Errorf("%s must not be zero", k)) + default: + // this validator should only ever be applied to TypeString, TypeInt and TypeFloat + panic(fmt.Errorf("can't use NoZeroValues with %T attribute %s", i, k)) + } + } + return +} + +// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value +// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive) +func CIDRNetwork(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + _, ipnet, err := net.ParseCIDR(v) + if err != nil { + es = append(es, fmt.Errorf( + "expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err)) + return + } + + if ipnet == nil || v != ipnet.String() { + es = append(es, fmt.Errorf( + "expected %s to contain a valid network CIDR, expected %s, got %s", + k, ipnet, v)) + } + + sigbits, _ := ipnet.Mask.Size() + if sigbits < min || sigbits > max { + es = append(es, fmt.Errorf( + "expected %q to contain a network CIDR with between %d and %d significant bits, got: %d", + k, min, max, sigbits)) + } + + return + } +} + +// ValidateJsonString is a SchemaValidateFunc which tests to make sure the +// supplied string is valid JSON. +func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) { + if _, err := structure.NormalizeJsonString(v); err != nil { + errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err)) + } + return +} + +// ValidateListUniqueStrings is a ValidateFunc that ensures a list has no +// duplicate items in it. It's useful for when a list is needed over a set +// because order matters, yet the items still need to be unique. +func ValidateListUniqueStrings(v interface{}, k string) (ws []string, errors []error) { + for n1, v1 := range v.([]interface{}) { + for n2, v2 := range v.([]interface{}) { + if v1.(string) == v2.(string) && n1 != n2 { + errors = append(errors, fmt.Errorf("%q: duplicate entry - %s", k, v1.(string))) + } + } + } + return +} + +// ValidateRegexp returns a SchemaValidateFunc which tests to make sure the +// supplied string is a valid regular expression. +func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) { + if _, err := regexp.Compile(v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: %s", k, err)) + } + return +} + +// ValidateRFC3339TimeString is a ValidateFunc that ensures a string parses +// as time.RFC3339 format +func ValidateRFC3339TimeString(v interface{}, k string) (ws []string, errors []error) { + if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: invalid RFC3339 timestamp", k)) + } + return +} diff --git a/vendor/vendor.json b/vendor/vendor.json index f0b2143..e5f9ed2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -3,10 +3,10 @@ "ignore": "test", "package": [ { - "checksumSHA1": "xrxkZvjRP9yQDW5wJ8Co7b10IX8=", + "checksumSHA1": "M3O9E6Z9qugnEsi74z6Asg6r46U=", "path": "cloud-deploy.io/go-st", - "revision": "17120a6d613348768fa5915ddd62f39e6851b1a5", - "revisionTime": "2016-11-26T17:29:32Z" + "revision": "0518a6e9eeb01f71abec82ecf50a7ce9d356a8c5", + "revisionTime": "2018-03-20T12:51:06Z" }, { "checksumSHA1": "jQh1fnoKPKMURvKkpdRjN695nAQ=", @@ -500,6 +500,12 @@ "revision": "26058acdefa6db8a258b554bf137a07d98c7ddc2", "revisionTime": "2018-03-01T14:13:39Z" }, + { + "checksumSHA1": "JL8eJjRMxGI2zz2fMYXhXQpRtPk=", + "path": "github.com/hashicorp/terraform/helper/validation", + "revision": "ecec59f0f0ad69350dfc8beb76c2d95cfd92f476", + "revisionTime": "2018-03-15T21:06:57Z" + }, { "checksumSHA1": "VgChgJYOTM4zrNOWifEqDsdv7Hk=", "path": "github.com/hashicorp/terraform/httpclient", @@ -1071,5 +1077,5 @@ "revisionTime": "2018-02-28T23:01:36Z" } ], - "rootPath": "bitbucket.org/morea/terraform-provider-ghost" + "rootPath": "cloud-deploy.io/terraform-provider-ghost" }