diff --git a/cloudstack/provider.go b/cloudstack/provider.go index c35a263c..dc437827 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -130,6 +130,9 @@ func Provider() terraform.ResourceProvider { "cloudstack_account": resourceCloudStackAccount(), "cloudstack_user": resourceCloudStackUser(), "cloudstack_domain": resourceCloudStackDomain(), + "cloudstack_pod": resourceCloudStackPod(), + "cloudstack_cluster": resourceCloudStackCluster(), + "cloudstack_physical_network": resourceCloudStackPhysicalNetwork(), }, ConfigureFunc: providerConfigure, diff --git a/cloudstack/resource_cloudstack_cluster.go b/cloudstack/resource_cloudstack_cluster.go new file mode 100644 index 00000000..d5f285af --- /dev/null +++ b/cloudstack/resource_cloudstack_cluster.go @@ -0,0 +1,253 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceCloudStackCluster() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackClusterCreate, + Read: resourceCloudStackClusterRead, + Update: resourceCloudStackClusterUpdate, + Delete: resourceCloudStackClusterDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "CloudManaged", + "ExternalManaged", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + + return + }, + }, + "hypervisor": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "XenServer", + "KVM", + "VMware", + "Hyperv", + "BareMetal", + "Simulator", + "Ovm3", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + }, + "pod_id": { + Type: schema.TypeString, + Required: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + }, + "allocation_state": { + Type: schema.TypeString, + Optional: true, + Default: "Enabled", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "Enabled", + "Disabled", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + "pod_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackClusterCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + clusterName := d.Get("name").(string) + clusterType := d.Get("type").(string) + hypervisor := d.Get("hypervisor").(string) + podID := d.Get("pod_id").(string) + zoneID := d.Get("zone_id").(string) + + // Create a new parameter struct + p := cs.Cluster.NewAddClusterParams( + clusterName, + clusterType, + hypervisor, + podID, + zoneID, + ) + + if allocationState, ok := d.GetOk("allocation_state"); ok { + p.SetAllocationstate(allocationState.(string)) + } + + log.Printf("[DEBUG] Creating Cluster %s", clusterName) + + c, err := cs.Cluster.AddCluster(p) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Cluster %s successfully created", clusterName) + + d.SetId(c.Id) + + return resourceCloudStackClusterRead(d, meta) +} + +func resourceCloudStackClusterRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] Retrieving Cluster %s", d.Get("name").(string)) + + // Get the Cluster details + c, count, err := cs.Cluster.GetClusterByName(d.Get("name").(string)) + + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Cluster %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return err + } + log.Printf("[DEBUG] Cluster %+v ", c) + d.SetId(c.Id) + d.Set("name", c.Name) + d.Set("type", c.Clustertype) + d.Set("hypervisor", c.Hypervisortype) + d.Set("pod_id", c.Podid) + d.Set("zone_id", c.Zoneid) + d.Set("allocation_state", c.Allocationstate) + d.Set("pod_name", c.Podname) + d.Set("zone_name", c.Zonename) + + return nil +} + +func resourceCloudStackClusterUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + if d.HasChange("allocation_state") { + log.Printf("[DEBUG] allocationState changed for cluster %s, starting update", name) + + p := cs.Cluster.NewUpdateClusterParams(d.Id()) + + p.SetAllocationstate(d.Get("allocation_state").(string)) + + _, err := cs.Cluster.UpdateCluster(p) + if err != nil { + return fmt.Errorf( + "Error updating the allocation_state for cluster %s: %s", name, err) + } + } + + if d.HasChange("name") { + log.Printf("[DEBUG] name changed for cluster %s, starting update", name) + + p := cs.Cluster.NewUpdateClusterParams(d.Id()) + + p.SetClustername(d.Get("name").(string)) + + _, err := cs.Cluster.UpdateCluster(p) + if err != nil { + return fmt.Errorf( + "Error updating the name for cluster %s: %s", name, err) + } + } + + if d.HasChange("type") { + log.Printf("[DEBUG] type changed for cluster %s, starting update", name) + + p := cs.Cluster.NewUpdateClusterParams(d.Id()) + + p.SetClustertype(d.Get("type").(string)) + + _, err := cs.Cluster.UpdateCluster(p) + if err != nil { + return fmt.Errorf( + "Error updating the type for cluster %s: %s", name, err) + } + } + + if d.HasChange("hypervisor") { + log.Printf("[DEBUG] hypervisor changed for cluster %s, starting update", name) + + p := cs.Cluster.NewUpdateClusterParams(d.Id()) + + p.SetHypervisor(d.Get("hypervisor").(string)) + + _, err := cs.Cluster.UpdateCluster(p) + if err != nil { + return fmt.Errorf( + "Error updating the hypervisor for cluster %s: %s", name, err) + } + } + return resourceCloudStackClusterRead(d, meta) +} + +func resourceCloudStackClusterDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Cluster.NewDeleteClusterParams(d.Id()) + _, err := cs.Cluster.DeleteCluster(p) + + if err != nil { + return fmt.Errorf("Error deleting Pod: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_physical_network.go b/cloudstack/resource_cloudstack_physical_network.go new file mode 100644 index 00000000..5d94693b --- /dev/null +++ b/cloudstack/resource_cloudstack_physical_network.go @@ -0,0 +1,320 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceCloudStackPhysicalNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackPhysicalNetworkCreate, + Read: resourceCloudStackPhysicalNetworkRead, + Update: resourceCloudStackPhysicalNetworkUpdate, + Delete: resourceCloudStackPhysicalNetworkDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + }, + "broadcast_domain_range": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "ZONE", + "POD", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + }, + "domain_id": { + Type: schema.TypeString, + Optional: true, + }, + "isolation_methods": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "VLAN", + "VXLAN", + "GRE", + "SST", + "BCF_SEGMENT", + "SSP", + "ODL", + "L3VPN", + "VCS", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + ForceNew: true, + }, + "network_speed": { + Type: schema.TypeString, + Optional: true, + }, + "vlan": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + vnetRange, _ := v.(string) + + ranges := strings.Split(vnetRange, ",") + for _, r := range ranges { + // Split the range into start and end + parts := strings.Split(r, "-") + if len(parts) != 2 { + errors = append(errors, fmt.Errorf("%q must consist of a range defined by two numbers separated by a dash, got %s", k, r)) + continue + } + + start, errStart := strconv.Atoi(parts[0]) + end, errEnd := strconv.Atoi(parts[1]) + if errStart != nil || errEnd != nil { + errors = append(errors, fmt.Errorf("%q contains non-numeric values in the range: %s", k, r)) + continue + } + + if start < 0 || start > 4095 || end < 0 || end > 4095 { + errors = append(errors, fmt.Errorf("%q numbers must be between 0 and 4095, got range: %s", k, r)) + continue + } + + if start > end { + errors = append(errors, fmt.Errorf("%q range start must be less than or equal to range end, got start: %d and end: %d", k, start, end)) + } + } + + return + }, + }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "zone_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Optional: true, + Default: "Enabled", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "Enabled", + "Disabled", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + }, + }, + } +} + +func resourceCloudStackPhysicalNetworkCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + zoneID := d.Get("zone_id").(string) + + // Create a new parameter struct + p := cs.Network.NewCreatePhysicalNetworkParams( + name, + zoneID, + ) + + if broadcastDomainRange, ok := d.GetOk("broadcast_domain_range"); ok { + p.SetBroadcastdomainrange(broadcastDomainRange.(string)) + } + + if domainID, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(domainID.(string)) + } + + if isolationMethods, ok := d.GetOk("isolation_methods"); ok { + p.SetIsolationmethods([]string{isolationMethods.(string)}) + } + + if tags, ok := d.GetOk("tags"); ok { + p.SetTags(convertToStringArray(tags.([]interface{}))) + } + + if networkSpeed, ok := d.GetOk("network_speed"); ok { + p.SetNetworkspeed(networkSpeed.(string)) + } + + if vlan, ok := d.GetOk("vlan"); ok { + p.SetVlan(vlan.(string)) + } + + log.Printf("[DEBUG] Creating Physical Network %s", name) + + n, err := cs.Network.CreatePhysicalNetwork(p) + + if err != nil { + return err + } + + if state, ok := d.GetOk("state"); ok { + p := cs.Network.NewUpdatePhysicalNetworkParams(n.Id) + log.Printf("[DEBUG] state changed for %s, starting update", name) + p.SetState(state.(string)) + _, err := cs.Network.UpdatePhysicalNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating the state %s: %s", name, err) + } + } + + log.Printf("[DEBUG] Physical Network %s successfully created", name) + + d.SetId(n.Id) + + return resourceCloudStackPhysicalNetworkRead(d, meta) +} + +func resourceCloudStackPhysicalNetworkRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] Retrieving Physical Network %s", d.Get("name").(string)) + + // Get the Physical Network details + p, count, err := cs.Network.GetPhysicalNetworkByName(d.Get("name").(string)) + + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Physical Network %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return err + } + + d.SetId(p.Id) + d.Set("name", p.Name) + d.Set("zone_id", p.Zoneid) + d.Set("broadcast_domain_range", p.Broadcastdomainrange) + d.Set("domain_id", p.Domainid) + d.Set("isolation_methods", p.Isolationmethods) + d.Set("network_speed", p.Networkspeed) + d.Set("vlan", p.Vlan) + d.Set("state", p.State) + d.Set("zone_name", p.Zonename) + d.Set("tags", p.Tags) + return nil +} + +func resourceCloudStackPhysicalNetworkUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + if d.HasChange("network_speed") { + p := cs.Network.NewUpdatePhysicalNetworkParams(d.Id()) + log.Printf("[DEBUG] network_speed changed for %s, starting update", name) + p.SetNetworkspeed(d.Get("network_speed").(string)) + _, err := cs.Network.UpdatePhysicalNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating the network_speed %s: %s", name, err) + } + } + if d.HasChange("state") { + p := cs.Network.NewUpdatePhysicalNetworkParams(d.Id()) + log.Printf("[DEBUG] state changed for %s, starting update", name) + p.SetState(d.Get("state").(string)) + _, err := cs.Network.UpdatePhysicalNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating the state %s: %s", name, err) + } + } + if d.HasChange("tags") { + p := cs.Network.NewUpdatePhysicalNetworkParams(d.Id()) + log.Printf("[DEBUG] tags changed for %s, starting update", name) + p.SetTags(convertToStringArray(d.Get("tags").([]interface{}))) + _, err := cs.Network.UpdatePhysicalNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating the tags %s: %s", name, err) + } + } + if d.HasChange("vlan") { + p := cs.Network.NewUpdatePhysicalNetworkParams(d.Id()) + log.Printf("[DEBUG] vlan changed for %s, starting update", name) + p.SetVlan(d.Get("vlan").(string)) + _, err := cs.Network.UpdatePhysicalNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating the vlan %s: %s", name, err) + } + } + + return resourceCloudStackPhysicalNetworkRead(d, meta) +} + +func resourceCloudStackPhysicalNetworkDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Network.NewDeletePhysicalNetworkParams(d.Id()) + _, err := cs.Network.DeletePhysicalNetwork(p) + + if err != nil { + return fmt.Errorf("Error deleting Physical Network: %s", err) + } + return nil +} + +func convertToStringArray(interfaces []interface{}) []string { + strings := make([]string, len(interfaces)) + for i, v := range interfaces { + strings[i] = v.(string) + } + return strings +} diff --git a/cloudstack/resource_cloudstack_pod.go b/cloudstack/resource_cloudstack_pod.go new file mode 100644 index 00000000..591909c2 --- /dev/null +++ b/cloudstack/resource_cloudstack_pod.go @@ -0,0 +1,238 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceCloudStackPod() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackPodCreate, + Read: resourceCloudStackPodRead, + Update: resourceCloudStackPodUpdate, + Delete: resourceCloudStackPodDelete, + Schema: map[string]*schema.Schema{ + "gateway": { + Type: schema.TypeString, + Required: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "netmask": { + Type: schema.TypeString, + Required: true, + }, + + "start_ip": { + Type: schema.TypeString, + Required: true, + }, + "end_ip": { + Type: schema.TypeString, + Optional: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "allocation_state": { + Type: schema.TypeString, + Optional: true, + Default: "Enabled", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + validOptions := []string{ + "Enabled", + "Disabled", + } + err := validateOptions(validOptions, v.(string), k) + if err != nil { + errors = append(errors, err) + } + return + }, + }, + "has_annotations": { + Type: schema.TypeBool, + Computed: true, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackPodCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + gateway := d.Get("gateway").(string) + name := d.Get("name").(string) + netmask := d.Get("netmask").(string) + startIP := d.Get("start_ip").(string) + zoneID := d.Get("zone_id").(string) + + // Create a new parameter struct + p := cs.Pod.NewCreatePodParams( + gateway, + name, + netmask, + startIP, + zoneID, + ) + + // If there is a end_ip supplied, add it to the parameter struct + if endIP, ok := d.GetOk("end_ip"); ok { + p.SetEndip(endIP.(string)) + } + + // If there is a end_ip supplied, add it to the parameter struct + if allocationState, ok := d.GetOk("allocation_state"); ok { + p.SetAllocationstate(allocationState.(string)) + } + + log.Printf("[DEBUG] Creating Pod %s", name) + + n, err := cs.Pod.CreatePod(p) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Pod %s successfully created", name) + + d.SetId(n.Id) + + return resourceCloudStackPodRead(d, meta) +} + +func resourceCloudStackPodRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] Retrieving Pod %s", d.Get("name").(string)) + + // Get the Pod details + p, count, err := cs.Pod.GetPodByName(d.Get("name").(string)) + + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Pod %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return err + } + + d.SetId(p.Id) + d.Set("allocation_state", p.Allocationstate) + d.Set("end_ip", p.Endip) + d.Set("gateway", p.Gateway) + d.Set("has_annotations", p.Hasannotations) + d.Set("netmask", p.Netmask) + d.Set("start_ip", p.Startip) + d.Set("zone_id", p.Zoneid) + d.Set("zone_name", p.Zonename) + return nil +} + +func resourceCloudStackPodUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + if d.HasChange("gateway") || d.HasChange("netmask") || d.HasChange("start_ip") || d.HasChange("end_ip") { + + p := cs.Pod.NewUpdatePodParams(d.Id()) + + if d.HasChange("gateway") { + log.Printf("[DEBUG] Gateway changed for %s, starting update", name) + p.SetGateway(d.Get("gateway").(string)) + } + if d.HasChange("netmask") { + log.Printf("[DEBUG] NetMask changed for %s, starting update", name) + p.SetNetmask(d.Get("netmask").(string)) + } + if d.HasChange("start_ip") { + log.Printf("[DEBUG] StartIP changed for %s, starting update", name) + p.SetStartip(d.Get("start_ip").(string)) + } + if d.HasChange("end_ip") { + log.Printf("[DEBUG] endIP changed for %s, starting update", name) + p.SetEndip(d.Get("end_ip").(string)) + } + + _, err := cs.Pod.UpdatePod(p) + if err != nil { + return fmt.Errorf( + "Error updating the pod %s: %s", name, err) + } + } + if d.HasChange("name") { + log.Printf("[DEBUG] Name changed for %s, starting update", name) + + p := cs.Pod.NewUpdatePodParams(d.Id()) + + p.SetName(d.Get("name").(string)) + + _, err := cs.Pod.UpdatePod(p) + if err != nil { + return fmt.Errorf( + "Error updating the name for pod %s: %s", name, err) + } + + } + + if d.HasChange("allocation_state") { + log.Printf("[DEBUG] allocationState changed for %s, starting update", name) + + p := cs.Pod.NewUpdatePodParams(d.Id()) + + p.SetAllocationstate(d.Get("allocation_state").(string)) + + _, err := cs.Pod.UpdatePod(p) + if err != nil { + return fmt.Errorf( + "Error updating the allocation_state for pod %s: %s", name, err) + } + } + + return resourceCloudStackPodRead(d, meta) +} + +func resourceCloudStackPodDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Pod.NewDeletePodParams(d.Id()) + _, err := cs.Pod.DeletePod(p) + + if err != nil { + return fmt.Errorf("Error deleting Pod: %s", err) + } + + return nil +} diff --git a/cloudstack/resources.go b/cloudstack/resources.go index 1b299589..36bbcc11 100644 --- a/cloudstack/resources.go +++ b/cloudstack/resources.go @@ -23,6 +23,7 @@ import ( "fmt" "log" "regexp" + "sort" "strings" "time" @@ -170,3 +171,13 @@ func importStatePassthrough(d *schema.ResourceData, meta interface{}) ([]*schema return []*schema.ResourceData{d}, nil } + +// validateOptions checks if a string is in a list. +func validateOptions(validOptions []string, input, k string) error { + sort.Strings(validOptions) + index := sort.SearchStrings(validOptions, input) + if index >= len(validOptions) || validOptions[index] != input { + return fmt.Errorf("%q must be one of %v got %v", k, validOptions, input) + } + return nil +}