Merge pull request #388 from hashicorp/f-elb-schema

aws_elb to helper/schema, fix issues in helper/schema around computed sets
This commit is contained in:
Mitchell Hashimoto 2014-10-10 10:03:35 -07:00
commit 1acb546d74
6 changed files with 349 additions and 302 deletions

View File

@ -43,6 +43,7 @@ func Provider() *schema.Provider {
ResourcesMap: map[string]*schema.Resource{
"aws_eip": resourceAwsEip(),
"aws_elb": resourceAwsElb(),
"aws_instance": resourceAwsInstance(),
"aws_security_group": resourceAwsSecurityGroup(),
"aws_db_subnet_group": resourceAwsDbSubnetGroup(),

View File

@ -1,369 +1,344 @@
package aws
import (
"bytes"
"fmt"
"log"
"strconv"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/elb"
)
func resource_aws_elb_create(
s *terraform.InstanceState,
d *terraform.InstanceDiff,
meta interface{}) (*terraform.InstanceState, error) {
func resourceAwsElb() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElbCreate,
Read: resourceAwsElbRead,
Update: resourceAwsElbUpdate,
Delete: resourceAwsElbDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"internal": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Computed: true,
},
"availability_zones": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ForceNew: true,
},
"instances": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
// TODO: could be not ForceNew
"security_groups": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ForceNew: true,
Computed: true,
},
// TODO: could be not ForceNew
"subnets": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ForceNew: true,
Computed: true,
},
// TODO: could be not ForceNew
"listener": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"instance_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"instance_protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"lb_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"lb_protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"ssl_certificate_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
Set: resourceAwsElbListenerHash,
},
// TODO: could be not ForceNew
"health_check": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"healthy_threshold": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"unhealthy_threshold": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"target": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"interval": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
Set: resourceAwsElbHealthCheckHash,
},
"dns_name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsElbHealthCheckHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%d-", m["healthy_threshold"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["unhealthy_threshold"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["target"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["interval"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["timeout"].(int)))
return hashcode.String(buf.String())
}
func resourceAwsElbListenerHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["instance_protocol"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["lb_protocol"].(string)))
if v, ok := m["ssl_certificate_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}
return hashcode.String(buf.String())
}
func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
elbconn := p.elbconn
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
// The name specified for the ELB. This is also our unique ID
// we save to state if the creation is successful (amazon verifies
// it is unique)
elbName := rs.Attributes["name"]
// Expand the "listener" array to goamz compat []elb.Listener
v := flatmap.Expand(rs.Attributes, "listener").([]interface{})
listeners, err := expandListeners(v)
// Expand the "listener" set to goamz compat []elb.Listener
listeners, err := expandListeners(d.Get("listener").(*schema.Set).List())
if err != nil {
return nil, err
return err
}
// Provision the elb
elbOpts := &elb.CreateLoadBalancer{
LoadBalancerName: elbName,
LoadBalancerName: d.Get("name").(string),
Listeners: listeners,
Internal: d.Get("internal").(bool),
}
if rs.Attributes["internal"] == "true" {
elbOpts.Internal = true
if v, ok := d.GetOk("availability_zones"); ok {
elbOpts.AvailZone = expandStringList(v.([]interface{}))
}
if _, ok := rs.Attributes["availability_zones.#"]; ok {
v = flatmap.Expand(rs.Attributes, "availability_zones").([]interface{})
elbOpts.AvailZone = expandStringList(v)
if v, ok := d.GetOk("security_groups"); ok {
elbOpts.SecurityGroups = expandStringList(v.([]interface{}))
}
if _, ok := rs.Attributes["security_groups.#"]; ok {
v = flatmap.Expand(rs.Attributes, "security_groups").([]interface{})
elbOpts.SecurityGroups = expandStringList(v)
}
if _, ok := rs.Attributes["subnets.#"]; ok {
v = flatmap.Expand(rs.Attributes, "subnets").([]interface{})
elbOpts.Subnets = expandStringList(v)
if v, ok := d.GetOk("subnets"); ok {
elbOpts.Subnets = expandStringList(v.([]interface{}))
}
log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
_, err = elbconn.CreateLoadBalancer(elbOpts)
if err != nil {
return nil, fmt.Errorf("Error creating ELB: %s", err)
if _, err := elbconn.CreateLoadBalancer(elbOpts); err != nil {
return fmt.Errorf("Error creating ELB: %s", err)
}
// Assign the elb's unique identifier for use later
rs.ID = elbName
log.Printf("[INFO] ELB ID: %s", elbName)
d.SetId(d.Get("name").(string))
log.Printf("[INFO] ELB ID: %s", d.Id())
if _, ok := rs.Attributes["instances.#"]; ok {
// If we have any instances, we need to register them
v = flatmap.Expand(rs.Attributes, "instances").([]interface{})
instances := expandStringList(v)
// Enable partial mode and record what we set
d.Partial(true)
d.SetPartial("name")
d.SetPartial("internal")
d.SetPartial("availability_zones")
d.SetPartial("security_groups")
d.SetPartial("subnets")
if len(instances) > 0 {
registerInstancesOpts := elb.RegisterInstancesWithLoadBalancer{
LoadBalancerName: elbName,
Instances: instances,
}
_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
if err != nil {
return rs, fmt.Errorf("Failure registering instances: %s", err)
}
}
}
if _, ok := rs.Attributes["health_check.#"]; ok {
v := flatmap.Expand(rs.Attributes, "health_check").([]interface{})
health_check := v[0].(map[string]interface{})
healthyThreshold, err := strconv.ParseInt(health_check["healthy_threshold"].(string), 0, 0)
unhealthyThreshold, err := strconv.ParseInt(health_check["unhealthy_threshold"].(string), 0, 0)
interval, err := strconv.ParseInt(health_check["interval"].(string), 0, 0)
timeout, err := strconv.ParseInt(health_check["timeout"].(string), 0, 0)
if err != nil {
return nil, err
}
if d.HasChange("health_check") {
vs := d.Get("health_check").(*schema.Set).List()
if len(vs) > 0 {
check := vs[0].(map[string]interface{})
configureHealthCheckOpts := elb.ConfigureHealthCheck{
LoadBalancerName: elbName,
LoadBalancerName: d.Id(),
Check: elb.HealthCheck{
HealthyThreshold: healthyThreshold,
UnhealthyThreshold: unhealthyThreshold,
Interval: interval,
Target: health_check["target"].(string),
Timeout: timeout,
HealthyThreshold: int64(check["healthy_threshold"].(int)),
UnhealthyThreshold: int64(check["unhealthy_threshold"].(int)),
Interval: int64(check["interval"].(int)),
Target: check["target"].(string),
Timeout: int64(check["timeout"].(int)),
},
}
_, err = elbconn.ConfigureHealthCheck(&configureHealthCheckOpts)
if err != nil {
return rs, fmt.Errorf("Failure configuring health check: %s", err)
return fmt.Errorf("Failure configuring health check: %s", err)
}
}
}
loadBalancer, err := resource_aws_elb_retrieve_balancer(rs.ID, elbconn)
if err != nil {
return rs, err
return resourceAwsElbUpdate(d, meta)
}
return resource_aws_elb_update_state(rs, loadBalancer)
}
func resource_aws_elb_update(
s *terraform.InstanceState,
d *terraform.InstanceDiff,
meta interface{}) (*terraform.InstanceState, error) {
func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
elbconn := p.elbconn
rs := s.MergeDiff(d)
d.Partial(true)
// If we currently have instances, or did have instances,
// we want to figure out what to add and remove from the load
// balancer
if attr, ok := d.Attributes["instances.#"]; ok && attr.Old != "" {
// The new state of instances merged with the diff
mergedInstances := expandStringList(flatmap.Expand(
rs.Attributes, "instances").([]interface{}))
if d.HasChange("instances") {
o, n := d.GetChange("instances")
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandStringList(os.Difference(ns).List())
add := expandStringList(ns.Difference(os).List())
// The state before the diff merge
previousInstances := expandStringList(flatmap.Expand(
s.Attributes, "instances").([]interface{}))
// keep track of what instances we are removing, and which
// we are adding
var toRemove []string
var toAdd []string
for _, instanceId := range mergedInstances {
for _, prevId := range previousInstances {
// If the merged instance ID existed
// previously, we don't have to do anything
if instanceId == prevId {
continue
// Otherwise, we need to add it to the load balancer
} else {
toAdd = append(toAdd, instanceId)
}
}
}
for i, instanceId := range toAdd {
for _, prevId := range previousInstances {
// If the instance ID we are adding existed
// previously, we want to not add it, but rather remove
// it
if instanceId == prevId {
toRemove = append(toRemove, instanceId)
toAdd = append(toAdd[:i], toAdd[i+1:]...)
// Otherwise, we continue adding it to the ELB
} else {
continue
}
}
}
if len(toAdd) > 0 {
if len(add) > 0 {
registerInstancesOpts := elb.RegisterInstancesWithLoadBalancer{
LoadBalancerName: rs.ID,
Instances: toAdd,
LoadBalancerName: d.Id(),
Instances: add,
}
_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
if err != nil {
return s, fmt.Errorf("Failure registering instances: %s", err)
return fmt.Errorf("Failure registering instances: %s", err)
}
}
if len(toRemove) > 0 {
if len(remove) > 0 {
deRegisterInstancesOpts := elb.DeregisterInstancesFromLoadBalancer{
LoadBalancerName: rs.ID,
Instances: toRemove,
LoadBalancerName: d.Id(),
Instances: remove,
}
_, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts)
if err != nil {
return s, fmt.Errorf("Failure deregistering instances: %s", err)
}
return fmt.Errorf("Failure deregistering instances: %s", err)
}
}
loadBalancer, err := resource_aws_elb_retrieve_balancer(rs.ID, elbconn)
if err != nil {
return s, err
d.SetPartial("instances")
}
return resource_aws_elb_update_state(rs, loadBalancer)
d.Partial(false)
return nil
}
func resource_aws_elb_destroy(
s *terraform.InstanceState,
meta interface{}) error {
func resourceAwsElbDelete(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
elbconn := p.elbconn
log.Printf("[INFO] Deleting ELB: %s", s.ID)
log.Printf("[INFO] Deleting ELB: %s", d.Id())
// Destroy the load balancer
deleteElbOpts := elb.DeleteLoadBalancer{
LoadBalancerName: s.ID,
LoadBalancerName: d.Id(),
}
_, err := elbconn.DeleteLoadBalancer(&deleteElbOpts)
if err != nil {
if _, err := elbconn.DeleteLoadBalancer(&deleteElbOpts); err != nil {
return fmt.Errorf("Error deleting ELB: %s", err)
}
return nil
}
func resource_aws_elb_refresh(
s *terraform.InstanceState,
meta interface{}) (*terraform.InstanceState, error) {
func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
elbconn := p.elbconn
loadBalancer, err := resource_aws_elb_retrieve_balancer(s.ID, elbconn)
// Retrieve the ELB properties for updating the state
describeElbOpts := &elb.DescribeLoadBalancer{
Names: []string{d.Id()},
}
describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts)
if err != nil {
return nil, err
return fmt.Errorf("Error retrieving ELB: %s", err)
}
if len(describeResp.LoadBalancers) != 1 {
return fmt.Errorf("Unable to find ELB: %#v", describeResp.LoadBalancers)
}
return resource_aws_elb_update_state(s, loadBalancer)
}
lb := describeResp.LoadBalancers[0]
func resource_aws_elb_diff(
s *terraform.InstanceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.InstanceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"name": diff.AttrTypeCreate,
"availability_zone": diff.AttrTypeCreate,
"security_groups": diff.AttrTypeCreate, // TODO could be AttrTypeUpdate
"subnets": diff.AttrTypeCreate, // TODO could be AttrTypeUpdate
"listener": diff.AttrTypeCreate,
"instances": diff.AttrTypeUpdate,
"health_check": diff.AttrTypeCreate,
"internal": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"dns_name",
},
}
return b.Diff(s, c)
}
func resource_aws_elb_update_state(
s *terraform.InstanceState,
balancer *elb.LoadBalancer) (*terraform.InstanceState, error) {
s.Attributes["name"] = balancer.LoadBalancerName
s.Attributes["dns_name"] = balancer.DNSName
if balancer.Scheme == "internal" {
s.Attributes["internal"] = "true"
}
// Flatten our group values
toFlatten := make(map[string]interface{})
if len(balancer.Instances) > 0 && balancer.Instances[0].InstanceId != "" {
toFlatten["instances"] = flattenInstances(balancer.Instances)
}
if len(balancer.SecurityGroups) > 0 && balancer.SecurityGroups[0] != "" {
toFlatten["security_groups"] = balancer.SecurityGroups
}
if len(balancer.Subnets) > 0 && balancer.Subnets[0] != "" {
toFlatten["subnets"] = balancer.Subnets
}
d.Set("name", lb.LoadBalancerName)
d.Set("dns_name", lb.DNSName)
d.Set("internal", lb.Scheme == "internal")
d.Set("instances", flattenInstances(lb.Instances))
d.Set("security_groups", lb.SecurityGroups)
d.Set("subnets", lb.Subnets)
// There's only one health check, so save that to state as we
// currently can
if balancer.HealthCheck.Target != "" {
toFlatten["health_check"] = flattenHealthCheck(balancer.HealthCheck)
if lb.HealthCheck.Target != "" {
d.Set("health_check", flattenHealthCheck(lb.HealthCheck))
}
for k, v := range flatmap.Flatten(toFlatten) {
s.Attributes[k] = v
}
return s, nil
}
// retrieves an ELB by its ID
func resource_aws_elb_retrieve_balancer(id string, elbconn *elb.ELB) (*elb.LoadBalancer, error) {
describeElbOpts := &elb.DescribeLoadBalancer{
Names: []string{id},
}
// Retrieve the ELB properties for updating the state
describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts)
if err != nil {
return nil, fmt.Errorf("Error retrieving ELB: %s", err)
}
loadBalancer := describeResp.LoadBalancers[0]
// Verify AWS returned our ELB
if len(describeResp.LoadBalancers) != 1 ||
describeResp.LoadBalancers[0].LoadBalancerName != id {
if err != nil {
return nil, fmt.Errorf("Unable to find ELB: %#v", describeResp.LoadBalancers)
}
}
return &loadBalancer, nil
}
func resource_aws_elb_validation() *config.Validator {
return &config.Validator{
Required: []string{
"name",
"listener.*",
"listener.*.instance_port",
"listener.*.instance_protocol",
"listener.*.lb_port",
"listener.*.lb_protocol",
},
Optional: []string{
"instances.*",
"listener.*.ssl_certificate_id",
"internal",
"availability_zones.*",
"security_groups.*",
"subnets.*",
"health_check.#",
"health_check.0.healthy_threshold",
"health_check.0.unhealthy_threshold",
"health_check.0.interval",
"health_check.0.target",
"health_check.0.timeout",
},
}
return nil
}

View File

@ -38,15 +38,6 @@ func init() {
Refresh: resource_aws_db_security_group_refresh,
},
"aws_elb": resource.Resource{
ConfigValidator: resource_aws_elb_validation(),
Create: resource_aws_elb_create,
Update: resource_aws_elb_update,
Destroy: resource_aws_elb_destroy,
Diff: resource_aws_elb_diff,
Refresh: resource_aws_elb_refresh,
},
"aws_internet_gateway": resource.Resource{
Create: resource_aws_internet_gateway_create,
Destroy: resource_aws_internet_gateway_destroy,

View File

@ -1,7 +1,6 @@
package aws
import (
"strconv"
"strings"
"github.com/mitchellh/goamz/autoscaling"
@ -16,28 +15,20 @@ func expandListeners(configured []interface{}) ([]elb.Listener, error) {
// Loop over our configured listeners and create
// an array of goamz compatabile objects
for _, listener := range configured {
newL := listener.(map[string]interface{})
instancePort, err := strconv.ParseInt(newL["instance_port"].(string), 0, 0)
lbPort, err := strconv.ParseInt(newL["lb_port"].(string), 0, 0)
if err != nil {
return nil, err
}
for _, lRaw := range configured {
data := lRaw.(map[string]interface{})
l := elb.Listener{
InstancePort: instancePort,
InstanceProtocol: newL["instance_protocol"].(string),
LoadBalancerPort: lbPort,
Protocol: newL["lb_protocol"].(string),
InstancePort: int64(data["instance_port"].(int)),
InstanceProtocol: data["instance_protocol"].(string),
LoadBalancerPort: int64(data["lb_port"].(int)),
Protocol: data["lb_protocol"].(string),
}
if attr, ok := newL["ssl_certificate_id"].(string); ok {
l.SSLCertificateId = attr
if v, ok := data["ssl_certificate_id"]; ok {
l.SSLCertificateId = v.(string)
}
listeners = append(listeners, l)
}

View File

@ -291,6 +291,13 @@ func (d *ResourceData) getSet(
return result
}
// If the entire list is computed, then the entire set is
// necessarilly computed.
if raw.Computed {
result.Computed = true
return result
}
list := raw.Value.([]interface{})
if len(list) == 0 {
if len(parts) > 0 {

View File

@ -641,6 +641,88 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{"${var.foo}", 1},
},
ConfigVariables: map[string]string{
"var.foo": "2" + config.InterpSplitDelim + "5",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, "${var.foo}"},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue +
config.InterpSplitDelim + "5",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{