Merge branch 'master' of https://github.com/hashicorp/terraform
This commit is contained in:
commit
f68c9eee63
|
@ -57,6 +57,9 @@ IMPROVEMENTS:
|
||||||
`www` instead of `www.example.com`.
|
`www` instead of `www.example.com`.
|
||||||
* providers/aws: Improve dependency violation error handling, when deleting
|
* providers/aws: Improve dependency violation error handling, when deleting
|
||||||
Internet Gateways or Auto Scaling groups [GH-1325].
|
Internet Gateways or Auto Scaling groups [GH-1325].
|
||||||
|
* provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade
|
||||||
|
`egine_version`, `parameter_group_name`, and `multi_az` without forcing
|
||||||
|
a new database to be created.[GH-1341]
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
|
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
"github.com/hashicorp/aws-sdk-go/gen/ec2"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/elb"
|
"github.com/hashicorp/aws-sdk-go/gen/elb"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/iam"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/s3"
|
"github.com/hashicorp/aws-sdk-go/gen/s3"
|
||||||
|
@ -30,6 +31,7 @@ type AWSClient struct {
|
||||||
r53conn *route53.Route53
|
r53conn *route53.Route53
|
||||||
region string
|
region string
|
||||||
rdsconn *rds.RDS
|
rdsconn *rds.RDS
|
||||||
|
iamconn *iam.IAM
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client configures and returns a fully initailized AWSClient
|
// Client configures and returns a fully initailized AWSClient
|
||||||
|
@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) {
|
||||||
client.r53conn = route53.New(creds, "us-east-1", nil)
|
client.r53conn = route53.New(creds, "us-east-1", nil)
|
||||||
log.Println("[INFO] Initializing EC2 Connection")
|
log.Println("[INFO] Initializing EC2 Connection")
|
||||||
client.ec2conn = ec2.New(creds, c.Region, nil)
|
client.ec2conn = ec2.New(creds, c.Region, nil)
|
||||||
|
|
||||||
|
client.iamconn = iam.New(creds, c.Region, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
|
|
@ -287,7 +287,12 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return resource.Retry(5*time.Minute, func() error {
|
||||||
|
if g, _ = getAwsAutoscalingGroup(d, meta); g != nil {
|
||||||
|
return fmt.Errorf("Auto Scaling Group still exists")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAwsAutoscalingGroup(
|
func getAwsAutoscalingGroup(
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
|
||||||
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
|
||||||
testAccCheckAWSAutoScalingGroupAttributes(&group),
|
testAccCheckAWSAutoScalingGroupAttributes(&group),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_autoscaling_group.bar", "availability_zones.1807834199", "us-west-2a"),
|
"aws_autoscaling_group.bar", "availability_zones.2487133097", "us-west-2a"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
"aws_autoscaling_group.bar", "name", "foobar3-terraform-test"),
|
"aws_autoscaling_group.bar", "name", "foobar3-terraform-test"),
|
||||||
resource.TestCheckResourceAttr(
|
resource.TestCheckResourceAttr(
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/aws-sdk-go/aws"
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/iam"
|
||||||
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: resourceAwsDbInstanceCreate,
|
Create: resourceAwsDbInstanceCreate,
|
||||||
Read: resourceAwsDbInstanceRead,
|
Read: resourceAwsDbInstanceRead,
|
||||||
|
Update: resourceAwsDbInstanceUpdate,
|
||||||
Delete: resourceAwsDbInstanceDelete,
|
Delete: resourceAwsDbInstanceDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
@ -47,7 +49,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
"engine_version": &schema.Schema{
|
"engine_version": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"storage_encrypted": &schema.Schema{
|
"storage_encrypted": &schema.Schema{
|
||||||
|
@ -119,7 +120,6 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"port": &schema.Schema{
|
"port": &schema.Schema{
|
||||||
|
@ -138,6 +138,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
"vpc_security_group_ids": &schema.Schema{
|
"vpc_security_group_ids": &schema.Schema{
|
||||||
Type: schema.TypeSet,
|
Type: schema.TypeSet,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
Set: func(v interface{}) int {
|
Set: func(v interface{}) int {
|
||||||
return hashcode.String(v.(string))
|
return hashcode.String(v.(string))
|
||||||
|
@ -162,13 +163,13 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"parameter_group_name": &schema.Schema{
|
"parameter_group_name": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
ForceNew: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"address": &schema.Schema{
|
"address": &schema.Schema{
|
||||||
|
@ -185,12 +186,24 @@ func resourceAwsDbInstance() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// apply_immediately is used to determine when the update modifications
|
||||||
|
// take place.
|
||||||
|
// See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html
|
||||||
|
"apply_immediately": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
conn := meta.(*AWSClient).rdsconn
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
|
||||||
opts := rds.CreateDBInstanceMessage{
|
opts := rds.CreateDBInstanceMessage{
|
||||||
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
|
AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)),
|
||||||
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
|
DBInstanceClass: aws.String(d.Get("instance_class").(string)),
|
||||||
|
@ -201,6 +214,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
|
||||||
Engine: aws.String(d.Get("engine").(string)),
|
Engine: aws.String(d.Get("engine").(string)),
|
||||||
EngineVersion: aws.String(d.Get("engine_version").(string)),
|
EngineVersion: aws.String(d.Get("engine_version").(string)),
|
||||||
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
|
StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)),
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr, ok := d.GetOk("storage_type"); ok {
|
if attr, ok := d.GetOk("storage_type"); ok {
|
||||||
|
@ -304,7 +318,11 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Set("name", *v.DBName)
|
if v.DBName != nil {
|
||||||
|
d.Set("name", *v.DBName)
|
||||||
|
} else {
|
||||||
|
d.Set("name", "")
|
||||||
|
}
|
||||||
d.Set("username", *v.MasterUsername)
|
d.Set("username", *v.MasterUsername)
|
||||||
d.Set("engine", *v.Engine)
|
d.Set("engine", *v.Engine)
|
||||||
d.Set("engine_version", *v.EngineVersion)
|
d.Set("engine_version", *v.EngineVersion)
|
||||||
|
@ -328,6 +346,28 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
d.Set("status", *v.DBInstanceStatus)
|
d.Set("status", *v.DBInstanceStatus)
|
||||||
d.Set("storage_encrypted", *v.StorageEncrypted)
|
d.Set("storage_encrypted", *v.StorageEncrypted)
|
||||||
|
|
||||||
|
// list tags for resource
|
||||||
|
// set tags
|
||||||
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
arn, err := buildRDSARN(d, meta)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] Error building ARN for DB Instance, not setting Tags for DB %s", *v.DBName)
|
||||||
|
} else {
|
||||||
|
resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceMessage{
|
||||||
|
ResourceName: aws.String(arn),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dt []rds.Tag
|
||||||
|
if len(resp.TagList) > 0 {
|
||||||
|
dt = resp.TagList
|
||||||
|
}
|
||||||
|
d.Set("tags", tagsToMapRDS(dt))
|
||||||
|
}
|
||||||
|
|
||||||
// Create an empty schema.Set to hold all vpc security group ids
|
// Create an empty schema.Set to hold all vpc security group ids
|
||||||
ids := &schema.Set{
|
ids := &schema.Set{
|
||||||
F: func(v interface{}) int {
|
F: func(v interface{}) int {
|
||||||
|
@ -390,6 +430,56 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
|
||||||
|
d.Partial(true)
|
||||||
|
// Change is used to determine if a ModifyDBInstanceMessage request actually
|
||||||
|
// gets sent.
|
||||||
|
change := false
|
||||||
|
|
||||||
|
req := &rds.ModifyDBInstanceMessage{
|
||||||
|
ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)),
|
||||||
|
DBInstanceIdentifier: aws.String(d.Id()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("engine_version") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("engine_version")
|
||||||
|
req.EngineVersion = aws.String(d.Get("engine_version").(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("multi_az") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("multi_az")
|
||||||
|
req.MultiAZ = aws.Boolean(d.Get("multi_az").(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("parameter_group_name") {
|
||||||
|
change = true
|
||||||
|
d.SetPartial("parameter_group_name")
|
||||||
|
req.DBParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if change {
|
||||||
|
log.Printf("[DEBUG] DB Instance Modification request: %#v", req)
|
||||||
|
_, err := conn.ModifyDBInstance(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error mofigying DB Instance %s: %s", d.Id(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if arn, err := buildRDSARN(d, meta); err == nil {
|
||||||
|
if err := setTagsRDS(conn, d, arn); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.Partial(false)
|
||||||
|
return resourceAwsDbInstanceRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func resourceAwsBbInstanceRetrieve(
|
func resourceAwsBbInstanceRetrieve(
|
||||||
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
|
d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) {
|
||||||
conn := meta.(*AWSClient).rdsconn
|
conn := meta.(*AWSClient).rdsconn
|
||||||
|
@ -439,3 +529,16 @@ func resourceAwsDbInstanceStateRefreshFunc(
|
||||||
return v, *v.DBInstanceStatus, nil
|
return v, *v.DBInstanceStatus, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRDSARN(d *schema.ResourceData, meta interface{}) (string, error) {
|
||||||
|
iamconn := meta.(*AWSClient).iamconn
|
||||||
|
region := meta.(*AWSClient).region
|
||||||
|
// An zero value GetUserRequest{} defers to the currently logged in user
|
||||||
|
resp, err := iamconn.GetUser(&iam.GetUserRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
user := resp.User
|
||||||
|
arn := fmt.Sprintf("arn:aws:rds:%s:%s:db:%s", region, *user.UserID, d.Id())
|
||||||
|
return arn, nil
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
||||||
return &schema.Resource{
|
return &schema.Resource{
|
||||||
Create: resourceAwsRoute53ZoneCreate,
|
Create: resourceAwsRoute53ZoneCreate,
|
||||||
Read: resourceAwsRoute53ZoneRead,
|
Read: resourceAwsRoute53ZoneRead,
|
||||||
|
Update: resourceAwsRoute53ZoneUpdate,
|
||||||
Delete: resourceAwsRoute53ZoneDelete,
|
Delete: resourceAwsRoute53ZoneDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
@ -29,6 +30,8 @@ func resourceAwsRoute53Zone() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tags": tagsSchema(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +75,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return resourceAwsRoute53ZoneUpdate(d, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
@ -87,9 +90,41 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get tags
|
||||||
|
req := &route53.ListTagsForResourceRequest{
|
||||||
|
ResourceID: aws.String(d.Id()),
|
||||||
|
ResourceType: aws.String("hostedzone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r53.ListTagsForResource(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []route53.Tag
|
||||||
|
if resp.ResourceTagSet != nil {
|
||||||
|
tags = resp.ResourceTagSet.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Set("tags", tagsToMapR53(tags)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
conn := meta.(*AWSClient).r53conn
|
||||||
|
|
||||||
|
if err := setTagsR53(conn, d); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.SetPartial("tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceAwsRoute53ZoneRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
r53 := meta.(*AWSClient).r53conn
|
r53 := meta.(*AWSClient).r53conn
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,9 @@ func TestCleanChangeID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccRoute53Zone(t *testing.T) {
|
func TestAccRoute53Zone(t *testing.T) {
|
||||||
|
var zone route53.HostedZone
|
||||||
|
var td route53.ResourceTagSet
|
||||||
|
|
||||||
resource.Test(t, resource.TestCase{
|
resource.Test(t, resource.TestCase{
|
||||||
PreCheck: func() { testAccPreCheck(t) },
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
Providers: testAccProviders,
|
Providers: testAccProviders,
|
||||||
|
@ -71,7 +74,9 @@ func TestAccRoute53Zone(t *testing.T) {
|
||||||
resource.TestStep{
|
resource.TestStep{
|
||||||
Config: testAccRoute53ZoneConfig,
|
Config: testAccRoute53ZoneConfig,
|
||||||
Check: resource.ComposeTestCheckFunc(
|
Check: resource.ComposeTestCheckFunc(
|
||||||
testAccCheckRoute53ZoneExists("aws_route53_zone.main"),
|
testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone),
|
||||||
|
testAccLoadTagsR53(&zone, &td),
|
||||||
|
testAccCheckTagsR53(&td.Tags, "foo", "bar"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -93,7 +98,7 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
rs, ok := s.RootModule().Resources[n]
|
rs, ok := s.RootModule().Resources[n]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -105,10 +110,34 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
||||||
_, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)})
|
resp, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Hosted zone err: %v", err)
|
return fmt.Errorf("Hosted zone err: %v", err)
|
||||||
}
|
}
|
||||||
|
*zone = *resp.HostedZone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).r53conn
|
||||||
|
|
||||||
|
zone := cleanZoneID(*zone.ID)
|
||||||
|
req := &route53.ListTagsForResourceRequest{
|
||||||
|
ResourceID: aws.String(zone),
|
||||||
|
ResourceType: aws.String("hostedzone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := conn.ListTagsForResource(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.ResourceTagSet != nil {
|
||||||
|
*td = *resp.ResourceTagSet
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,5 +145,10 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc {
|
||||||
const testAccRoute53ZoneConfig = `
|
const testAccRoute53ZoneConfig = `
|
||||||
resource "aws_route53_zone" "main" {
|
resource "aws_route53_zone" "main" {
|
||||||
name = "hashicorp.com"
|
name = "hashicorp.com"
|
||||||
|
|
||||||
|
tags {
|
||||||
|
foo = "bar"
|
||||||
|
Name = "tf-route53-tag-test"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -88,14 +88,12 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s3conn.GetBucketTagging(&s3.GetBucketTaggingRequest{
|
tagSet, err := getTagSetS3(s3conn, d.Id())
|
||||||
Bucket: aws.String(d.Id()),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.Set("tags", tagsToMapS3(resp.TagSet)); err != nil {
|
if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,3 +110,22 @@ func tagsToMapS3(ts []s3.Tag) map[string]string {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return a slice of s3 tags associated with the given s3 bucket. Essentially
|
||||||
|
// s3.GetBucketTagging, except returns an empty slice instead of an error when
|
||||||
|
// there are no tags.
|
||||||
|
func getTagSetS3(s3conn *s3.S3, bucket string) ([]s3.Tag, error) {
|
||||||
|
request := &s3.GetBucketTaggingRequest{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s3conn.GetBucketTagging(request)
|
||||||
|
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "NoSuchTagSet" {
|
||||||
|
// There is no tag set associated with the bucket.
|
||||||
|
return []s3.Tag{}, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.TagSet, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setTags is a helper to set the tags for a resource. It expects the
|
||||||
|
// tags field to be named "tags"
|
||||||
|
func setTagsRDS(conn *rds.RDS, d *schema.ResourceData, arn string) error {
|
||||||
|
if d.HasChange("tags") {
|
||||||
|
oraw, nraw := d.GetChange("tags")
|
||||||
|
o := oraw.(map[string]interface{})
|
||||||
|
n := nraw.(map[string]interface{})
|
||||||
|
create, remove := diffTagsRDS(tagsFromMapRDS(o), tagsFromMapRDS(n))
|
||||||
|
|
||||||
|
// Set tags
|
||||||
|
if len(remove) > 0 {
|
||||||
|
log.Printf("[DEBUG] Removing tags: %#v", remove)
|
||||||
|
k := make([]string, len(remove), len(remove))
|
||||||
|
for i, t := range remove {
|
||||||
|
k[i] = *t.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.RemoveTagsFromResource(&rds.RemoveTagsFromResourceMessage{
|
||||||
|
ResourceName: aws.String(arn),
|
||||||
|
TagKeys: k,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(create) > 0 {
|
||||||
|
log.Printf("[DEBUG] Creating tags: %#v", create)
|
||||||
|
err := conn.AddTagsToResource(&rds.AddTagsToResourceMessage{
|
||||||
|
ResourceName: aws.String(arn),
|
||||||
|
Tags: create,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// diffTags takes our tags locally and the ones remotely and returns
|
||||||
|
// the set of tags that must be created, and the set of tags that must
|
||||||
|
// be destroyed.
|
||||||
|
func diffTagsRDS(oldTags, newTags []rds.Tag) ([]rds.Tag, []rds.Tag) {
|
||||||
|
// First, we're creating everything we have
|
||||||
|
create := make(map[string]interface{})
|
||||||
|
for _, t := range newTags {
|
||||||
|
create[*t.Key] = *t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of what to remove
|
||||||
|
var remove []rds.Tag
|
||||||
|
for _, t := range oldTags {
|
||||||
|
old, ok := create[*t.Key]
|
||||||
|
if !ok || old != *t.Value {
|
||||||
|
// Delete it!
|
||||||
|
remove = append(remove, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsFromMapRDS(create), remove
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagsFromMap returns the tags for the given map of data.
|
||||||
|
func tagsFromMapRDS(m map[string]interface{}) []rds.Tag {
|
||||||
|
result := make([]rds.Tag, 0, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
result = append(result, rds.Tag{
|
||||||
|
Key: aws.String(k),
|
||||||
|
Value: aws.String(v.(string)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagsToMap turns the list of tags into a map.
|
||||||
|
func tagsToMapRDS(ts []rds.Tag) map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
for _, t := range ts {
|
||||||
|
result[*t.Key] = *t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/rds"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiffRDSTags(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Old, New map[string]interface{}
|
||||||
|
Create, Remove map[string]string
|
||||||
|
}{
|
||||||
|
// Basic add/remove
|
||||||
|
{
|
||||||
|
Old: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
New: map[string]interface{}{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
Create: map[string]string{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
Remove: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Modify
|
||||||
|
{
|
||||||
|
Old: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
New: map[string]interface{}{
|
||||||
|
"foo": "baz",
|
||||||
|
},
|
||||||
|
Create: map[string]string{
|
||||||
|
"foo": "baz",
|
||||||
|
},
|
||||||
|
Remove: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
c, r := diffTagsRDS(tagsFromMapRDS(tc.Old), tagsFromMapRDS(tc.New))
|
||||||
|
cm := tagsToMapRDS(c)
|
||||||
|
rm := tagsToMapRDS(r)
|
||||||
|
if !reflect.DeepEqual(cm, tc.Create) {
|
||||||
|
t.Fatalf("%d: bad create: %#v", i, cm)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rm, tc.Remove) {
|
||||||
|
t.Fatalf("%d: bad remove: %#v", i, rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testAccCheckTags can be used to check the tags on a resource.
|
||||||
|
func testAccCheckRDSTags(
|
||||||
|
ts *[]rds.Tag, key string, value string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
m := tagsToMapRDS(*ts)
|
||||||
|
v, ok := m[key]
|
||||||
|
if value != "" && !ok {
|
||||||
|
return fmt.Errorf("Missing tag: %s", key)
|
||||||
|
} else if value == "" && ok {
|
||||||
|
return fmt.Errorf("Extra tag: %s", key)
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != value {
|
||||||
|
return fmt.Errorf("%s: bad value: %s", key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/aws-sdk-go/aws"
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setTags is a helper to set the tags for a resource. It expects the
|
||||||
|
// tags field to be named "tags"
|
||||||
|
func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error {
|
||||||
|
if d.HasChange("tags") {
|
||||||
|
oraw, nraw := d.GetChange("tags")
|
||||||
|
o := oraw.(map[string]interface{})
|
||||||
|
n := nraw.(map[string]interface{})
|
||||||
|
create, remove := diffTagsR53(tagsFromMapR53(o), tagsFromMapR53(n))
|
||||||
|
|
||||||
|
// Set tags
|
||||||
|
r := make([]string, len(remove))
|
||||||
|
for i, t := range remove {
|
||||||
|
r[i] = *t.Key
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:%#v", create, remove)
|
||||||
|
req := &route53.ChangeTagsForResourceRequest{
|
||||||
|
AddTags: create,
|
||||||
|
RemoveTagKeys: r,
|
||||||
|
ResourceID: aws.String(d.Id()),
|
||||||
|
ResourceType: aws.String("hostedzone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.ChangeTagsForResource(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// diffTags takes our tags locally and the ones remotely and returns
|
||||||
|
// the set of tags that must be created, and the set of tags that must
|
||||||
|
// be destroyed.
|
||||||
|
func diffTagsR53(oldTags, newTags []route53.Tag) ([]route53.Tag, []route53.Tag) {
|
||||||
|
// First, we're creating everything we have
|
||||||
|
create := make(map[string]interface{})
|
||||||
|
for _, t := range newTags {
|
||||||
|
create[*t.Key] = *t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of what to remove
|
||||||
|
var remove []route53.Tag
|
||||||
|
for _, t := range oldTags {
|
||||||
|
old, ok := create[*t.Key]
|
||||||
|
if !ok || old != *t.Value {
|
||||||
|
// Delete it!
|
||||||
|
remove = append(remove, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsFromMapR53(create), remove
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagsFromMap returns the tags for the given map of data.
|
||||||
|
func tagsFromMapR53(m map[string]interface{}) []route53.Tag {
|
||||||
|
result := make([]route53.Tag, 0, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
result = append(result, route53.Tag{
|
||||||
|
Key: aws.String(k),
|
||||||
|
Value: aws.String(v.(string)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagsToMap turns the list of tags into a map.
|
||||||
|
func tagsToMapR53(ts []route53.Tag) map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
for _, t := range ts {
|
||||||
|
result[*t.Key] = *t.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/aws-sdk-go/gen/route53"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiffTagsR53(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Old, New map[string]interface{}
|
||||||
|
Create, Remove map[string]string
|
||||||
|
}{
|
||||||
|
// Basic add/remove
|
||||||
|
{
|
||||||
|
Old: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
New: map[string]interface{}{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
Create: map[string]string{
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
Remove: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Modify
|
||||||
|
{
|
||||||
|
Old: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
New: map[string]interface{}{
|
||||||
|
"foo": "baz",
|
||||||
|
},
|
||||||
|
Create: map[string]string{
|
||||||
|
"foo": "baz",
|
||||||
|
},
|
||||||
|
Remove: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
c, r := diffTagsR53(tagsFromMapR53(tc.Old), tagsFromMapR53(tc.New))
|
||||||
|
cm := tagsToMapR53(c)
|
||||||
|
rm := tagsToMapR53(r)
|
||||||
|
if !reflect.DeepEqual(cm, tc.Create) {
|
||||||
|
t.Fatalf("%d: bad create: %#v", i, cm)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rm, tc.Remove) {
|
||||||
|
t.Fatalf("%d: bad remove: %#v", i, rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testAccCheckTags can be used to check the tags on a resource.
|
||||||
|
func testAccCheckTagsR53(
|
||||||
|
ts *[]route53.Tag, key string, value string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
m := tagsToMapR53(*ts)
|
||||||
|
v, ok := m[key]
|
||||||
|
if value != "" && !ok {
|
||||||
|
return fmt.Errorf("Missing tag: %s", key)
|
||||||
|
} else if value == "" && ok {
|
||||||
|
return fmt.Errorf("Extra tag: %s", key)
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != value {
|
||||||
|
return fmt.Errorf("%s: bad value: %s", key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,11 @@ func testAccCheckComputeV2FloatingIPExists(t *testing.T, n string, kp *floatingi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var testAccComputeV2FloatingIP_basic = fmt.Sprintf(`
|
var testAccComputeV2FloatingIP_basic = `
|
||||||
resource "openstack_compute_floatingip_v2" "foo" {
|
resource "openstack_compute_floatingip_v2" "foo" {
|
||||||
}`)
|
}
|
||||||
|
|
||||||
|
resource "openstack_compute_instance_v2" "bar" {
|
||||||
|
name = "terraform-acc-floating-ip-test"
|
||||||
|
floating_ip = "${openstack_compute_floatingip_v2.foo.address}"
|
||||||
|
}`
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
|
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
|
||||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||||
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
|
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
|
||||||
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
|
|
||||||
"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
|
"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
|
||||||
"github.com/rackspace/gophercloud/pagination"
|
"github.com/rackspace/gophercloud/pagination"
|
||||||
)
|
)
|
||||||
|
@ -303,7 +302,7 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
|
||||||
}
|
}
|
||||||
err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), server.ID)
|
err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), server.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err)
|
return fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -770,44 +769,18 @@ func extractFloatingIPFromIP(ips []floatingips.FloatingIP, IP string) *floatingi
|
||||||
}
|
}
|
||||||
|
|
||||||
func assignFloatingIP(networkingClient *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, instanceID string) error {
|
func assignFloatingIP(networkingClient *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, instanceID string) error {
|
||||||
networkID, err := getFirstNetworkID(networkingClient, instanceID)
|
portID, err := getInstancePortID(networkingClient, instanceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
portID, err := getInstancePortID(networkingClient, instanceID, networkID)
|
return floatingips.Update(networkingClient, floatingIP.ID, floatingips.UpdateOpts{
|
||||||
_, err = floatingips.Update(networkingClient, floatingIP.ID, floatingips.UpdateOpts{
|
|
||||||
PortID: portID,
|
PortID: portID,
|
||||||
}).Extract()
|
}).Err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstNetworkID(networkingClient *gophercloud.ServiceClient, instanceID string) (string, error) {
|
func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID string) (string, error) {
|
||||||
pager := networks.List(networkingClient, networks.ListOpts{})
|
|
||||||
|
|
||||||
var networkdID string
|
|
||||||
err := pager.EachPage(func(page pagination.Page) (bool, error) {
|
|
||||||
networkList, err := networks.ExtractNetworks(page)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(networkList) > 0 {
|
|
||||||
networkdID = networkList[0].ID
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("No network found for the instance %s", instanceID)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return networkdID, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID, networkID string) (string, error) {
|
|
||||||
pager := ports.List(networkingClient, ports.ListOpts{
|
pager := ports.List(networkingClient, ports.ListOpts{
|
||||||
DeviceID: instanceID,
|
DeviceID: instanceID,
|
||||||
NetworkID: networkID,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var portID string
|
var portID string
|
||||||
|
@ -826,6 +799,11 @@ func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if portID == "" {
|
||||||
|
return "", fmt.Errorf("Cannot find port for instance %s", instanceID)
|
||||||
|
}
|
||||||
|
|
||||||
return portID, nil
|
return portID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,10 @@ func resourceNetworkingFloatingIPV2() *schema.Resource {
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
"pool": &schema.Schema{
|
"pool": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
|
DefaultFunc: envDefaultFunc("OS_POOL_NAME"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,9 +81,11 @@ func testAccCheckNetworkingV2FloatingIPExists(t *testing.T, n string, kp *floati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var testAccNetworkingV2FloatingIP_basic = fmt.Sprintf(`
|
var testAccNetworkingV2FloatingIP_basic = `
|
||||||
resource "openstack_networking_floatingip_v2" "foo" {
|
resource "openstack_networking_floatingip_v2" "foo" {
|
||||||
region = "%s"
|
}
|
||||||
pool = "%s"
|
|
||||||
}`,
|
resource "openstack_compute_instance_v2" "bar" {
|
||||||
OS_REGION_NAME, OS_POOL_NAME)
|
name = "terraform-acc-floating-ip-test"
|
||||||
|
floating_ip = "${openstack_networking_floatingip_v2.foo.address}"
|
||||||
|
}`
|
||||||
|
|
|
@ -93,6 +93,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Build the context based on the arguments given
|
||||||
ctx, planned, err := c.Context(contextOpts{
|
ctx, planned, err := c.Context(contextOpts{
|
||||||
|
Destroy: c.Destroy,
|
||||||
Path: configPath,
|
Path: configPath,
|
||||||
StatePath: c.Meta.statePath,
|
StatePath: c.Meta.statePath,
|
||||||
})
|
})
|
||||||
|
@ -140,12 +141,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts terraform.PlanOpts
|
if _, err := ctx.Plan(); err != nil {
|
||||||
if c.Destroy {
|
|
||||||
opts.Destroy = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := ctx.Plan(&opts); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Error creating plan: %s", err))
|
"Error creating plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -319,6 +315,10 @@ Options:
|
||||||
"-state". This can be used to preserve the old
|
"-state". This can be used to preserve the old
|
||||||
state.
|
state.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
@ -357,6 +357,10 @@ Options:
|
||||||
"-state". This can be used to preserve the old
|
"-state". This can be used to preserve the old
|
||||||
state.
|
state.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,96 @@ func TestApply_destroyPlan(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApply_destroyTargeted(t *testing.T) {
|
||||||
|
originalState := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "i-ab123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"test_load_balancer.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_load_balancer",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "lb-abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
Destroy: true,
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the apply command pointing to our existing state
|
||||||
|
args := []string{
|
||||||
|
"-force",
|
||||||
|
"-target", "test_instance.foo",
|
||||||
|
"-state", statePath,
|
||||||
|
testFixturePath("apply-destroy-targeted"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a new state exists
|
||||||
|
if _, err := os.Stat(statePath); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
state, err := terraform.ReadState(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
t.Fatal("state should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
actualStr := strings.TrimSpace(state.String())
|
||||||
|
expectedStr := strings.TrimSpace(testApplyDestroyStr)
|
||||||
|
if actualStr != expectedStr {
|
||||||
|
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have a backup file
|
||||||
|
f, err = os.Open(statePath + DefaultBackupExtention)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupState, err := terraform.ReadState(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualStr = strings.TrimSpace(backupState.String())
|
||||||
|
expectedStr = strings.TrimSpace(originalState.String())
|
||||||
|
if actualStr != expectedStr {
|
||||||
|
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testApplyDestroyStr = `
|
const testApplyDestroyStr = `
|
||||||
<no state>
|
<no state>
|
||||||
`
|
`
|
||||||
|
|
|
@ -85,3 +85,17 @@ func loadKVFile(rawPath string) (map[string]string, error) {
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlagStringSlice is a flag.Value implementation for parsing targets from the
|
||||||
|
// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar
|
||||||
|
|
||||||
|
type FlagStringSlice []string
|
||||||
|
|
||||||
|
func (v *FlagStringSlice) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (v *FlagStringSlice) Set(raw string) error {
|
||||||
|
*v = append(*v, raw)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -38,6 +38,9 @@ type Meta struct {
|
||||||
input bool
|
input bool
|
||||||
variables map[string]string
|
variables map[string]string
|
||||||
|
|
||||||
|
// Targets for this context (private)
|
||||||
|
targets []string
|
||||||
|
|
||||||
color bool
|
color bool
|
||||||
oldUi cli.Ui
|
oldUi cli.Ui
|
||||||
|
|
||||||
|
@ -126,6 +129,9 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
||||||
m.statePath = copts.StatePath
|
m.statePath = copts.StatePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tell the context if we're in a destroy plan / apply
|
||||||
|
opts.Destroy = copts.Destroy
|
||||||
|
|
||||||
// Store the loaded state
|
// Store the loaded state
|
||||||
state, err := m.State()
|
state, err := m.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -267,6 +273,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
vs[k] = v
|
vs[k] = v
|
||||||
}
|
}
|
||||||
opts.Variables = vs
|
opts.Variables = vs
|
||||||
|
opts.Targets = m.targets
|
||||||
opts.UIInput = m.UIInput()
|
opts.UIInput = m.UIInput()
|
||||||
|
|
||||||
return &opts
|
return &opts
|
||||||
|
@ -278,6 +285,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||||
f.BoolVar(&m.input, "input", true, "input")
|
f.BoolVar(&m.input, "input", true, "input")
|
||||||
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
||||||
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
||||||
|
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
||||||
|
|
||||||
if m.autoKey != "" {
|
if m.autoKey != "" {
|
||||||
f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
|
f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
|
||||||
|
@ -388,4 +396,7 @@ type contextOpts struct {
|
||||||
|
|
||||||
// GetMode is the module.GetMode to use when loading the module tree.
|
// GetMode is the module.GetMode to use when loading the module tree.
|
||||||
GetMode module.GetMode
|
GetMode module.GetMode
|
||||||
|
|
||||||
|
// Set to true when running a destroy plan/apply.
|
||||||
|
Destroy bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ type PlanCommand struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PlanCommand) Run(args []string) int {
|
func (c *PlanCommand) Run(args []string) int {
|
||||||
var destroy, refresh bool
|
var destroy, refresh, detailed bool
|
||||||
var outPath string
|
var outPath string
|
||||||
var moduleDepth int
|
var moduleDepth int
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&outPath, "out", "", "path")
|
cmdFlags.StringVar(&outPath, "out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -53,6 +54,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, _, err := c.Context(contextOpts{
|
ctx, _, err := c.Context(contextOpts{
|
||||||
|
Destroy: destroy,
|
||||||
Path: path,
|
Path: path,
|
||||||
StatePath: c.Meta.statePath,
|
StatePath: c.Meta.statePath,
|
||||||
})
|
})
|
||||||
|
@ -86,7 +88,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy})
|
plan, err := ctx.Plan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -128,6 +130,9 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
ModuleDepth: moduleDepth,
|
ModuleDepth: moduleDepth,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if detailed {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +156,12 @@ Options:
|
||||||
-destroy If set, a plan will be generated to destroy all resources
|
-destroy If set, a plan will be generated to destroy all resources
|
||||||
managed by the given configuration and state.
|
managed by the given configuration and state.
|
||||||
|
|
||||||
|
-detailed-exitcode Return detailed exit codes when the command exits. This
|
||||||
|
will change the meaning of exit codes to:
|
||||||
|
0 - Succeeded, diff is empty (no changes)
|
||||||
|
1 - Errored
|
||||||
|
2 - Succeeded, there is a diff
|
||||||
|
|
||||||
-input=true Ask for input for variables if not directly set.
|
-input=true Ask for input for variables if not directly set.
|
||||||
|
|
||||||
-module-depth=n Specifies the depth of modules to show in the output.
|
-module-depth=n Specifies the depth of modules to show in the output.
|
||||||
|
@ -168,6 +179,10 @@ Options:
|
||||||
up Terraform-managed resources. By default it will
|
up Terraform-managed resources. By default it will
|
||||||
use the state "terraform.tfstate" if it exists.
|
use the state "terraform.tfstate" if it exists.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -567,6 +567,56 @@ func TestPlan_disableBackup(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlan_detailedExitcode(t *testing.T) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(testFixturePath("plan")); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(cwd)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"-detailed-exitcode"}
|
||||||
|
if code := c.Run(args); code != 2 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(testFixturePath("plan-emptydiff")); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(cwd)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"-detailed-exitcode"}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const planVarFile = `
|
const planVarFile = `
|
||||||
foo = "bar"
|
foo = "bar"
|
||||||
`
|
`
|
||||||
|
|
|
@ -135,6 +135,10 @@ Options:
|
||||||
-state-out=path Path to write updated state file. By default, the
|
-state-out=path Path to write updated state file. By default, the
|
||||||
"-state" path will be used.
|
"-state" path will be used.
|
||||||
|
|
||||||
|
-target=resource Resource to target. Operation will be limited to this
|
||||||
|
resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_load_balancer" "foo" {
|
||||||
|
instances = ["${test_instance.foo.*.id}"]
|
||||||
|
}
|
93
dag/dag.go
93
dag/dag.go
|
@ -17,6 +17,40 @@ type AcyclicGraph struct {
|
||||||
// WalkFunc is the callback used for walking the graph.
|
// WalkFunc is the callback used for walking the graph.
|
||||||
type WalkFunc func(Vertex) error
|
type WalkFunc func(Vertex) error
|
||||||
|
|
||||||
|
// Returns a Set that includes every Vertex yielded by walking down from the
|
||||||
|
// provided starting Vertex v.
|
||||||
|
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
|
||||||
|
s := new(Set)
|
||||||
|
start := asVertexList(g.DownEdges(v))
|
||||||
|
memoFunc := func(v Vertex) error {
|
||||||
|
s.Add(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.depthFirstWalk(start, memoFunc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a Set that includes every Vertex yielded by walking up from the
|
||||||
|
// provided starting Vertex v.
|
||||||
|
func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) {
|
||||||
|
s := new(Set)
|
||||||
|
start := asVertexList(g.UpEdges(v))
|
||||||
|
memoFunc := func(v Vertex) error {
|
||||||
|
s.Add(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Root returns the root of the DAG, or an error.
|
// Root returns the root of the DAG, or an error.
|
||||||
//
|
//
|
||||||
// Complexity: O(V)
|
// Complexity: O(V)
|
||||||
|
@ -61,15 +95,11 @@ func (g *AcyclicGraph) TransitiveReduction() {
|
||||||
|
|
||||||
for _, u := range g.Vertices() {
|
for _, u := range g.Vertices() {
|
||||||
uTargets := g.DownEdges(u)
|
uTargets := g.DownEdges(u)
|
||||||
vs := make([]Vertex, uTargets.Len())
|
vs := asVertexList(g.DownEdges(u))
|
||||||
for i, vRaw := range uTargets.List() {
|
|
||||||
vs[i] = vRaw.(Vertex)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.depthFirstWalk(vs, func(v Vertex) error {
|
g.depthFirstWalk(vs, func(v Vertex) error {
|
||||||
shared := uTargets.Intersection(g.DownEdges(v))
|
shared := uTargets.Intersection(g.DownEdges(v))
|
||||||
for _, raw := range shared.List() {
|
for _, vPrime := range asVertexList(shared) {
|
||||||
vPrime := raw.(Vertex)
|
|
||||||
g.RemoveEdge(BasicEdge(u, vPrime))
|
g.RemoveEdge(BasicEdge(u, vPrime))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,12 +175,10 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
for _, v := range vertices {
|
for _, v := range vertices {
|
||||||
// Build our list of dependencies and the list of channels to
|
// Build our list of dependencies and the list of channels to
|
||||||
// wait on until we start executing for this vertex.
|
// wait on until we start executing for this vertex.
|
||||||
depsRaw := g.DownEdges(v).List()
|
deps := asVertexList(g.DownEdges(v))
|
||||||
deps := make([]Vertex, len(depsRaw))
|
|
||||||
depChs := make([]<-chan struct{}, len(deps))
|
depChs := make([]<-chan struct{}, len(deps))
|
||||||
for i, raw := range depsRaw {
|
for i, dep := range deps {
|
||||||
deps[i] = raw.(Vertex)
|
depChs[i] = vertMap[dep]
|
||||||
depChs[i] = vertMap[deps[i]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get our channel so that we can close it when we're done
|
// Get our channel so that we can close it when we're done
|
||||||
|
@ -200,6 +228,16 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simple convenience helper for converting a dag.Set to a []Vertex
|
||||||
|
func asVertexList(s *Set) []Vertex {
|
||||||
|
rawList := s.List()
|
||||||
|
vertexList := make([]Vertex, len(rawList))
|
||||||
|
for i, raw := range rawList {
|
||||||
|
vertexList[i] = raw.(Vertex)
|
||||||
|
}
|
||||||
|
return vertexList
|
||||||
|
}
|
||||||
|
|
||||||
// depthFirstWalk does a depth-first walk of the graph starting from
|
// depthFirstWalk does a depth-first walk of the graph starting from
|
||||||
// the vertices in start. This is not exported now but it would make sense
|
// the vertices in start. This is not exported now but it would make sense
|
||||||
// to export this publicly at some point.
|
// to export this publicly at some point.
|
||||||
|
@ -233,3 +271,36 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
|
||||||
|
// the vertices in start.
|
||||||
|
func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
seen := make(map[Vertex]struct{})
|
||||||
|
frontier := make([]Vertex, len(start))
|
||||||
|
copy(frontier, start)
|
||||||
|
for len(frontier) > 0 {
|
||||||
|
// Pop the current vertex
|
||||||
|
n := len(frontier)
|
||||||
|
current := frontier[n-1]
|
||||||
|
frontier = frontier[:n-1]
|
||||||
|
|
||||||
|
// Check if we've seen this already and return...
|
||||||
|
if _, ok := seen[current]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[current] = struct{}{}
|
||||||
|
|
||||||
|
// Visit the current node
|
||||||
|
if err := cb(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit targets of this in reverse order.
|
||||||
|
targets := g.UpEdges(current).List()
|
||||||
|
for i := len(targets) - 1; i >= 0; i-- {
|
||||||
|
frontier = append(frontier, targets[i].(Vertex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -126,6 +126,68 @@ func TestAcyclicGraphValidate_cycleSelf(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAcyclicGraphAncestors(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Add(4)
|
||||||
|
g.Add(5)
|
||||||
|
g.Connect(BasicEdge(0, 1))
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.Connect(BasicEdge(3, 4))
|
||||||
|
g.Connect(BasicEdge(4, 5))
|
||||||
|
|
||||||
|
actual, err := g.Ancestors(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []Vertex{3, 4, 5}
|
||||||
|
|
||||||
|
if actual.Len() != len(expected) {
|
||||||
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range expected {
|
||||||
|
if !actual.Include(e) {
|
||||||
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcyclicGraphDescendents(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Add(4)
|
||||||
|
g.Add(5)
|
||||||
|
g.Connect(BasicEdge(0, 1))
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.Connect(BasicEdge(3, 4))
|
||||||
|
g.Connect(BasicEdge(4, 5))
|
||||||
|
|
||||||
|
actual, err := g.Descendents(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []Vertex{0, 1}
|
||||||
|
|
||||||
|
if actual.Len() != len(expected) {
|
||||||
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range expected {
|
||||||
|
if !actual.Include(e) {
|
||||||
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAcyclicGraphWalk(t *testing.T) {
|
func TestAcyclicGraphWalk(t *testing.T) {
|
||||||
var g AcyclicGraph
|
var g AcyclicGraph
|
||||||
g.Add(1)
|
g.Add(1)
|
||||||
|
|
|
@ -190,6 +190,7 @@ func testStep(
|
||||||
// Build the context
|
// Build the context
|
||||||
opts.Module = mod
|
opts.Module = mod
|
||||||
opts.State = state
|
opts.State = state
|
||||||
|
opts.Destroy = step.Destroy
|
||||||
ctx := terraform.NewContext(&opts)
|
ctx := terraform.NewContext(&opts)
|
||||||
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
||||||
estrs := make([]string, len(es))
|
estrs := make([]string, len(es))
|
||||||
|
@ -209,7 +210,7 @@ func testStep(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan!
|
// Plan!
|
||||||
if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil {
|
if p, err := ctx.Plan(); err != nil {
|
||||||
return state, fmt.Errorf(
|
return state, fmt.Errorf(
|
||||||
"Error planning: %s", err)
|
"Error planning: %s", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,6 +33,7 @@ const (
|
||||||
// ContextOpts are the user-configurable options to create a context with
|
// ContextOpts are the user-configurable options to create a context with
|
||||||
// NewContext.
|
// NewContext.
|
||||||
type ContextOpts struct {
|
type ContextOpts struct {
|
||||||
|
Destroy bool
|
||||||
Diff *Diff
|
Diff *Diff
|
||||||
Hooks []Hook
|
Hooks []Hook
|
||||||
Module *module.Tree
|
Module *module.Tree
|
||||||
|
@ -40,6 +41,7 @@ type ContextOpts struct {
|
||||||
State *State
|
State *State
|
||||||
Providers map[string]ResourceProviderFactory
|
Providers map[string]ResourceProviderFactory
|
||||||
Provisioners map[string]ResourceProvisionerFactory
|
Provisioners map[string]ResourceProvisionerFactory
|
||||||
|
Targets []string
|
||||||
Variables map[string]string
|
Variables map[string]string
|
||||||
|
|
||||||
UIInput UIInput
|
UIInput UIInput
|
||||||
|
@ -49,6 +51,7 @@ type ContextOpts struct {
|
||||||
// perform operations on infrastructure. This structure is built using
|
// perform operations on infrastructure. This structure is built using
|
||||||
// NewContext. See the documentation for that.
|
// NewContext. See the documentation for that.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
destroy bool
|
||||||
diff *Diff
|
diff *Diff
|
||||||
diffLock sync.RWMutex
|
diffLock sync.RWMutex
|
||||||
hooks []Hook
|
hooks []Hook
|
||||||
|
@ -58,6 +61,7 @@ type Context struct {
|
||||||
sh *stopHook
|
sh *stopHook
|
||||||
state *State
|
state *State
|
||||||
stateLock sync.RWMutex
|
stateLock sync.RWMutex
|
||||||
|
targets []string
|
||||||
uiInput UIInput
|
uiInput UIInput
|
||||||
variables map[string]string
|
variables map[string]string
|
||||||
|
|
||||||
|
@ -95,12 +99,14 @@ func NewContext(opts *ContextOpts) *Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Context{
|
return &Context{
|
||||||
|
destroy: opts.Destroy,
|
||||||
diff: opts.Diff,
|
diff: opts.Diff,
|
||||||
hooks: hooks,
|
hooks: hooks,
|
||||||
module: opts.Module,
|
module: opts.Module,
|
||||||
providers: opts.Providers,
|
providers: opts.Providers,
|
||||||
provisioners: opts.Provisioners,
|
provisioners: opts.Provisioners,
|
||||||
state: state,
|
state: state,
|
||||||
|
targets: opts.Targets,
|
||||||
uiInput: opts.UIInput,
|
uiInput: opts.UIInput,
|
||||||
variables: opts.Variables,
|
variables: opts.Variables,
|
||||||
|
|
||||||
|
@ -135,6 +141,8 @@ func (c *Context) GraphBuilder() GraphBuilder {
|
||||||
Providers: providers,
|
Providers: providers,
|
||||||
Provisioners: provisioners,
|
Provisioners: provisioners,
|
||||||
State: c.state,
|
State: c.state,
|
||||||
|
Targets: c.targets,
|
||||||
|
Destroy: c.destroy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +261,7 @@ func (c *Context) Apply() (*State, error) {
|
||||||
//
|
//
|
||||||
// Plan also updates the diff of this context to be the diff generated
|
// Plan also updates the diff of this context to be the diff generated
|
||||||
// by the plan, so Apply can be called after.
|
// by the plan, so Apply can be called after.
|
||||||
func (c *Context) Plan(opts *PlanOpts) (*Plan, error) {
|
func (c *Context) Plan() (*Plan, error) {
|
||||||
v := c.acquireRun()
|
v := c.acquireRun()
|
||||||
defer c.releaseRun(v)
|
defer c.releaseRun(v)
|
||||||
|
|
||||||
|
@ -264,7 +272,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var operation walkOperation
|
var operation walkOperation
|
||||||
if opts != nil && opts.Destroy {
|
if c.destroy {
|
||||||
operation = walkPlanDestroy
|
operation = walkPlanDestroy
|
||||||
} else {
|
} else {
|
||||||
// Set our state to be something temporary. We do this so that
|
// Set our state to be something temporary. We do this so that
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -65,6 +65,13 @@ type BuiltinGraphBuilder struct {
|
||||||
|
|
||||||
// Provisioners is the list of provisioners supported.
|
// Provisioners is the list of provisioners supported.
|
||||||
Provisioners []string
|
Provisioners []string
|
||||||
|
|
||||||
|
// Targets is the user-specified list of resources to target.
|
||||||
|
Targets []string
|
||||||
|
|
||||||
|
// Destroy is set to true when we're in a `terraform destroy` or a
|
||||||
|
// `terraform plan -destroy`
|
||||||
|
Destroy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds the graph according to the steps returned by Steps.
|
// Build builds the graph according to the steps returned by Steps.
|
||||||
|
@ -82,7 +89,11 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
return []GraphTransformer{
|
return []GraphTransformer{
|
||||||
// Create all our resources from the configuration and state
|
// Create all our resources from the configuration and state
|
||||||
&ConfigTransformer{Module: b.Root},
|
&ConfigTransformer{Module: b.Root},
|
||||||
&OrphanTransformer{State: b.State, Module: b.Root},
|
&OrphanTransformer{
|
||||||
|
State: b.State,
|
||||||
|
Module: b.Root,
|
||||||
|
Targeting: (len(b.Targets) > 0),
|
||||||
|
},
|
||||||
|
|
||||||
// Provider-related transformations
|
// Provider-related transformations
|
||||||
&MissingProviderTransformer{Providers: b.Providers},
|
&MissingProviderTransformer{Providers: b.Providers},
|
||||||
|
@ -104,6 +115,10 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Optionally reduces the graph to a user-specified list of targets and
|
||||||
|
// their dependencies.
|
||||||
|
&TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy},
|
||||||
|
|
||||||
// Create the destruction nodes
|
// Create the destruction nodes
|
||||||
&DestroyTransformer{},
|
&DestroyTransformer{},
|
||||||
&CreateBeforeDestroyTransformer{},
|
&CreateBeforeDestroyTransformer{},
|
||||||
|
|
|
@ -21,6 +21,26 @@ type graphNodeConfig interface {
|
||||||
GraphNodeDependent
|
GraphNodeDependent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable is an interface that all graph nodes for the
|
||||||
|
// configuration graph need to implement in order to be be addressed / targeted
|
||||||
|
// properly.
|
||||||
|
type GraphNodeAddressable interface {
|
||||||
|
graphNodeConfig
|
||||||
|
|
||||||
|
ResourceAddress() *ResourceAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable is an interface for graph nodes to implement when they
|
||||||
|
// need to be told about incoming targets. This is useful for nodes that need
|
||||||
|
// to respect targets as they dynamically expand. Note that the list of targets
|
||||||
|
// provided will contain every target provided, and each implementing graph
|
||||||
|
// node must filter this list to targets considered relevant.
|
||||||
|
type GraphNodeTargetable interface {
|
||||||
|
GraphNodeAddressable
|
||||||
|
|
||||||
|
SetTargets([]ResourceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeConfigModule represents a module within the configuration graph.
|
// GraphNodeConfigModule represents a module within the configuration graph.
|
||||||
type GraphNodeConfigModule struct {
|
type GraphNodeConfigModule struct {
|
||||||
Path []string
|
Path []string
|
||||||
|
@ -191,6 +211,9 @@ type GraphNodeConfigResource struct {
|
||||||
// If this is set to anything other than destroyModeNone, then this
|
// If this is set to anything other than destroyModeNone, then this
|
||||||
// resource represents a resource that will be destroyed in some way.
|
// resource represents a resource that will be destroyed in some way.
|
||||||
DestroyMode GraphNodeDestroyMode
|
DestroyMode GraphNodeDestroyMode
|
||||||
|
|
||||||
|
// Used during DynamicExpand to target indexes
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *GraphNodeConfigResource) DependableName() []string {
|
func (n *GraphNodeConfigResource) DependableName() []string {
|
||||||
|
@ -279,6 +302,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
steps = append(steps, &ResourceCountTransformer{
|
steps = append(steps, &ResourceCountTransformer{
|
||||||
Resource: n.Resource,
|
Resource: n.Resource,
|
||||||
Destroy: n.DestroyMode != DestroyNone,
|
Destroy: n.DestroyMode != DestroyNone,
|
||||||
|
Targets: n.Targets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +313,9 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
// expand orphans, which have all the same semantics in a destroy
|
// expand orphans, which have all the same semantics in a destroy
|
||||||
// as a primary.
|
// as a primary.
|
||||||
steps = append(steps, &OrphanTransformer{
|
steps = append(steps, &OrphanTransformer{
|
||||||
State: state,
|
State: state,
|
||||||
View: n.Resource.Id(),
|
View: n.Resource.Id(),
|
||||||
|
Targeting: (len(n.Targets) > 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
steps = append(steps, &DeposedTransformer{
|
steps = append(steps, &DeposedTransformer{
|
||||||
|
@ -314,6 +339,22 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
return b.Build(ctx.Path())
|
return b.Build(ctx.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
|
||||||
|
return &ResourceAddress{
|
||||||
|
// Indicates no specific index; will match on other three fields
|
||||||
|
Index: -1,
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable impl.
|
||||||
|
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
|
||||||
|
n.Targets = targets
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeEvalable impl.
|
// GraphNodeEvalable impl.
|
||||||
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
||||||
return &EvalSequence{
|
return &EvalSequence{
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
//go:generate stringer -type=InstanceType instancetype.go
|
||||||
|
|
||||||
|
// InstanceType is an enum of the various types of instances store in the State
|
||||||
|
type InstanceType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeInvalid InstanceType = iota
|
||||||
|
TypePrimary
|
||||||
|
TypeTainted
|
||||||
|
TypeDeposed
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
// generated by stringer -type=InstanceType instancetype.go; DO NOT EDIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed"
|
||||||
|
|
||||||
|
var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44}
|
||||||
|
|
||||||
|
func (i InstanceType) String() string {
|
||||||
|
if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) {
|
||||||
|
return fmt.Sprintf("InstanceType(%d)", i)
|
||||||
|
}
|
||||||
|
return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]]
|
||||||
|
}
|
|
@ -18,15 +18,6 @@ func init() {
|
||||||
gob.Register(make(map[string]string))
|
gob.Register(make(map[string]string))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanOpts are the options used to generate an execution plan for
|
|
||||||
// Terraform.
|
|
||||||
type PlanOpts struct {
|
|
||||||
// If set to true, then the generated plan will destroy all resources
|
|
||||||
// that are created. Otherwise, it will move towards the desired state
|
|
||||||
// specified in the configuration.
|
|
||||||
Destroy bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan represents a single Terraform execution plan, which contains
|
// Plan represents a single Terraform execution plan, which contains
|
||||||
// all the information necessary to make an infrastructure change.
|
// all the information necessary to make an infrastructure change.
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceAddress is a way of identifying an individual resource (or,
|
||||||
|
// eventually, a subset of resources) within the state. It is used for Targets.
|
||||||
|
type ResourceAddress struct {
|
||||||
|
Index int
|
||||||
|
InstanceType InstanceType
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
||||||
|
matches, err := tokenizeResourceAddress(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resourceIndex := -1
|
||||||
|
if matches["index"] != "" {
|
||||||
|
var err error
|
||||||
|
if resourceIndex, err = strconv.Atoi(matches["index"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instanceType := TypePrimary
|
||||||
|
if matches["instance_type"] != "" {
|
||||||
|
var err error
|
||||||
|
if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: resourceIndex,
|
||||||
|
InstanceType: instanceType,
|
||||||
|
Name: matches["name"],
|
||||||
|
Type: matches["type"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (addr *ResourceAddress) Equals(raw interface{}) bool {
|
||||||
|
other, ok := raw.(*ResourceAddress)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMatch := (addr.Index == -1 ||
|
||||||
|
other.Index == -1 ||
|
||||||
|
addr.Index == other.Index)
|
||||||
|
|
||||||
|
return (indexMatch &&
|
||||||
|
addr.InstanceType == other.InstanceType &&
|
||||||
|
addr.Name == other.Name &&
|
||||||
|
addr.Type == other.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseInstanceType(s string) (InstanceType, error) {
|
||||||
|
switch s {
|
||||||
|
case "primary":
|
||||||
|
return TypePrimary, nil
|
||||||
|
case "deposed":
|
||||||
|
return TypeDeposed, nil
|
||||||
|
case "tainted":
|
||||||
|
return TypeTainted, nil
|
||||||
|
default:
|
||||||
|
return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenizeResourceAddress(s string) (map[string]string, error) {
|
||||||
|
// Example of portions of the regexp below using the
|
||||||
|
// string "aws_instance.web.tainted[1]"
|
||||||
|
re := regexp.MustCompile(`\A` +
|
||||||
|
// "aws_instance"
|
||||||
|
`(?P<type>\w+)\.` +
|
||||||
|
// "web"
|
||||||
|
`(?P<name>\w+)` +
|
||||||
|
// "tainted" (optional, omission implies: "primary")
|
||||||
|
`(?:\.(?P<instance_type>\w+))?` +
|
||||||
|
// "1" (optional, omission implies: "0")
|
||||||
|
`(?:\[(?P<index>\d+)\])?` +
|
||||||
|
`\z`)
|
||||||
|
groupNames := re.SubexpNames()
|
||||||
|
rawMatches := re.FindAllStringSubmatch(s, -1)
|
||||||
|
if len(rawMatches) != 1 {
|
||||||
|
return nil, fmt.Errorf("Problem parsing address: %q", s)
|
||||||
|
}
|
||||||
|
matches := make(map[string]string)
|
||||||
|
for i, m := range rawMatches[0] {
|
||||||
|
matches[groupNames[i]] = m
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseResourceAddress(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Input string
|
||||||
|
Expected *ResourceAddress
|
||||||
|
}{
|
||||||
|
"implicit primary, no specific index": {
|
||||||
|
Input: "aws_instance.foo",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"implicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"explicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo.primary[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tainted": {
|
||||||
|
Input: "aws_instance.foo.tainted",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"deposed": {
|
||||||
|
Input: "aws_instance.foo.deposed",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeDeposed,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
out, err := ParseResourceAddress(tc.Input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(out, tc.Expected) {
|
||||||
|
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceAddressEquals(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Address *ResourceAddress
|
||||||
|
Other interface{}
|
||||||
|
Expect bool
|
||||||
|
}{
|
||||||
|
"basic match": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"address does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"other does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"neither sets index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"different type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_vpc",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different name": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "bar",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different instance type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
actual := tc.Address.Equals(tc.Other)
|
||||||
|
if actual != tc.Expect {
|
||||||
|
t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v",
|
||||||
|
tn, tc.Expect, actual, tc.Address, tc.Other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
count = 3
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "${aws_instance.foo.num}"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
resource "aws_vpc" "metoo" {}
|
||||||
|
resource "aws_instance" "notme" { }
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
vpc_id = "${aws_vpc.metoo.id}"
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
resource "aws_elb" "meneither" {
|
||||||
|
instances = ["${aws_instance.me.*.id}"]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
resource "aws_vpc" "metoo" {}
|
||||||
|
resource "aws_instance" "notme" { }
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
vpc_id = "${aws_vpc.metoo.id}"
|
||||||
|
}
|
||||||
|
resource "aws_elb" "meneither" {
|
||||||
|
instances = ["${aws_instance.me.*.id}"]
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
resource "aws_vpc" "me" {}
|
||||||
|
|
||||||
|
resource "aws_subnet" "me" {
|
||||||
|
vpc_id = "${aws_vpc.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
subnet_id = "${aws_subnet.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_vpc" "notme" {}
|
||||||
|
resource "aws_subnet" "notme" {}
|
||||||
|
resource "aws_instance" "notme" {}
|
||||||
|
resource "aws_instance" "notmeeither" {
|
||||||
|
name = "${aws_instance.me.id}"
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
resource "aws_vpc" "notme" {}
|
||||||
|
|
||||||
|
resource "aws_subnet" "notme" {
|
||||||
|
vpc_id = "${aws_vpc.notme.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
subnet_id = "${aws_subnet.notme.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "notme" {}
|
||||||
|
resource "aws_instance" "metoo" {
|
||||||
|
name = "${aws_instance.me.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_elb" "me" {
|
||||||
|
instances = "${aws_instance.me.*.id}"
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
@ -25,6 +26,11 @@ type OrphanTransformer struct {
|
||||||
// using the graph path.
|
// using the graph path.
|
||||||
Module *module.Tree
|
Module *module.Tree
|
||||||
|
|
||||||
|
// Targets are user-specified resources to target. We need to be aware of
|
||||||
|
// these so we don't improperly identify orphans when they've just been
|
||||||
|
// filtered out of the graph via targeting.
|
||||||
|
Targeting bool
|
||||||
|
|
||||||
// View, if non-nil will set a view on the module state.
|
// View, if non-nil will set a view on the module state.
|
||||||
View string
|
View string
|
||||||
}
|
}
|
||||||
|
@ -35,6 +41,13 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Targeting {
|
||||||
|
log.Printf("Skipping orphan transformer because we have targets.")
|
||||||
|
// If we are in a run where we are targeting nodes, we won't process
|
||||||
|
// orphans for this run.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Build up all our state representatives
|
// Build up all our state representatives
|
||||||
resourceRep := make(map[string]struct{})
|
resourceRep := make(map[string]struct{})
|
||||||
for _, v := range g.Vertices() {
|
for _, v := range g.Vertices() {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type ResourceCountTransformer struct {
|
type ResourceCountTransformer struct {
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
|
@ -27,7 +28,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each count, build and add the node
|
// For each count, build and add the node
|
||||||
nodes := make([]dag.Vertex, count)
|
nodes := make([]dag.Vertex, 0, count)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
// Set the index. If our count is 1 we special case it so that
|
// Set the index. If our count is 1 we special case it so that
|
||||||
// we handle the "resource.0" and "resource" boundary properly.
|
// we handle the "resource.0" and "resource" boundary properly.
|
||||||
|
@ -49,9 +50,14 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip nodes if targeting excludes them
|
||||||
|
if !t.nodeIsTargeted(node) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Add the node now
|
// Add the node now
|
||||||
nodes[i] = node
|
nodes = append(nodes, node)
|
||||||
g.Add(nodes[i])
|
g.Add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the dependency connections
|
// Make the dependency connections
|
||||||
|
@ -64,6 +70,25 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
|
||||||
|
// no targets specified, everything stays in the graph
|
||||||
|
if len(t.Targets) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
addressable, ok := node.(GraphNodeAddressable)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := addressable.ResourceAddress()
|
||||||
|
for _, targetAddr := range t.Targets {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type graphNodeExpandedResource struct {
|
type graphNodeExpandedResource struct {
|
||||||
Index int
|
Index int
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
|
@ -77,6 +102,23 @@ func (n *graphNodeExpandedResource) Name() string {
|
||||||
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress {
|
||||||
|
// We want this to report the logical index properly, so we must undo the
|
||||||
|
// special case from the expand
|
||||||
|
index := n.Index
|
||||||
|
if index == -1 {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: index,
|
||||||
|
// TODO: kjkjkj
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeDependable impl.
|
// GraphNodeDependable impl.
|
||||||
func (n *graphNodeExpandedResource) DependableName() []string {
|
func (n *graphNodeExpandedResource) DependableName() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import "github.com/hashicorp/terraform/dag"
|
||||||
|
|
||||||
|
// TargetsTransformer is a GraphTransformer that, when the user specifies a
|
||||||
|
// list of resources to target, limits the graph to only those resources and
|
||||||
|
// their dependencies.
|
||||||
|
type TargetsTransformer struct {
|
||||||
|
// List of targeted resource names specified by the user
|
||||||
|
Targets []string
|
||||||
|
|
||||||
|
// Set to true when we're in a `terraform destroy` or a
|
||||||
|
// `terraform plan -destroy`
|
||||||
|
Destroy bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) Transform(g *Graph) error {
|
||||||
|
if len(t.Targets) > 0 {
|
||||||
|
// TODO: duplicated in OrphanTransformer; pull up parsing earlier
|
||||||
|
addrs, err := t.parseTargetAddresses()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedNodes, err := t.selectTargetedNodes(g, addrs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
if targetedNodes.Include(v) {
|
||||||
|
} else {
|
||||||
|
g.Remove(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) {
|
||||||
|
addrs := make([]ResourceAddress, len(t.Targets))
|
||||||
|
for i, target := range t.Targets {
|
||||||
|
ta, err := ParseResourceAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addrs[i] = *ta
|
||||||
|
}
|
||||||
|
return addrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) selectTargetedNodes(
|
||||||
|
g *Graph, addrs []ResourceAddress) (*dag.Set, error) {
|
||||||
|
targetedNodes := new(dag.Set)
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
// Keep all providers; they'll be pruned later if necessary
|
||||||
|
if r, ok := v.(GraphNodeProvider); ok {
|
||||||
|
targetedNodes.Add(r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the remaining filter, we only care about addressable nodes
|
||||||
|
r, ok := v.(GraphNodeAddressable)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.nodeIsTarget(r, addrs) {
|
||||||
|
targetedNodes.Add(r)
|
||||||
|
// If the node would like to know about targets, tell it.
|
||||||
|
if n, ok := r.(GraphNodeTargetable); ok {
|
||||||
|
n.SetTargets(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deps *dag.Set
|
||||||
|
var err error
|
||||||
|
if t.Destroy {
|
||||||
|
deps, err = g.Descendents(r)
|
||||||
|
} else {
|
||||||
|
deps, err = g.Ancestors(r)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range deps.List() {
|
||||||
|
targetedNodes.Add(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetedNodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) nodeIsTarget(
|
||||||
|
r GraphNodeAddressable, addrs []ResourceAddress) bool {
|
||||||
|
addr := r.ResourceAddress()
|
||||||
|
for _, targetAddr := range addrs {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargetsTransformer(t *testing.T) {
|
||||||
|
mod := testModule(t, "transform-targets-basic")
|
||||||
|
|
||||||
|
g := Graph{Path: RootModulePath}
|
||||||
|
{
|
||||||
|
tf := &ConfigTransformer{Module: mod}
|
||||||
|
if err := tf.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
transform := &TargetsTransformer{Targets: []string{"aws_instance.me"}}
|
||||||
|
if err := transform.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(`
|
||||||
|
aws_instance.me
|
||||||
|
aws_subnet.me
|
||||||
|
aws_subnet.me
|
||||||
|
aws_vpc.me
|
||||||
|
aws_vpc.me
|
||||||
|
`)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetsTransformer_destroy(t *testing.T) {
|
||||||
|
mod := testModule(t, "transform-targets-destroy")
|
||||||
|
|
||||||
|
g := Graph{Path: RootModulePath}
|
||||||
|
{
|
||||||
|
tf := &ConfigTransformer{Module: mod}
|
||||||
|
if err := tf.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
transform := &TargetsTransformer{
|
||||||
|
Targets: []string{"aws_instance.me"},
|
||||||
|
Destroy: true,
|
||||||
|
}
|
||||||
|
if err := transform.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(`
|
||||||
|
aws_elb.me
|
||||||
|
aws_instance.me
|
||||||
|
aws_instance.me
|
||||||
|
aws_instance.metoo
|
||||||
|
aws_instance.me
|
||||||
|
`)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||||
* `-state-out=path` - Path to write updated state file. By default, the
|
* `-state-out=path` - Path to write updated state file. By default, the
|
||||||
`-state` path will be used.
|
`-state` path will be used.
|
||||||
|
|
||||||
|
* `-target=resource` - A [Resource
|
||||||
|
Address](/docs/internals/resource-addressing.html) to target. Operation will
|
||||||
|
be limited to this resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,9 @@ confirmation before destroying.
|
||||||
This command accepts all the flags that the
|
This command accepts all the flags that the
|
||||||
[apply command](/docs/commands/apply.html) accepts. If `-force` is
|
[apply command](/docs/commands/apply.html) accepts. If `-force` is
|
||||||
set, then the destroy confirmation will not be shown.
|
set, then the destroy confirmation will not be shown.
|
||||||
|
|
||||||
|
The `-target` flag, instead of affecting "dependencies" will instead also
|
||||||
|
destroy any resources that _depend on_ the target(s) specified.
|
||||||
|
|
||||||
|
The behavior of any `terraform destroy` command can be previewed at any time
|
||||||
|
with an equivalent `terraform plan -destroy` command.
|
||||||
|
|
|
@ -28,6 +28,13 @@ The command-line flags are all optional. The list of available flags are:
|
||||||
|
|
||||||
* `-destroy` - If set, generates a plan to destroy all the known resources.
|
* `-destroy` - If set, generates a plan to destroy all the known resources.
|
||||||
|
|
||||||
|
* `-detailed-exitcode` - Return a detailed exit code when the command exits.
|
||||||
|
When provided, this argument changes the exit codes and their meanings to
|
||||||
|
provide more granular information about what the resulting plan contains:
|
||||||
|
* 0 = Succeeded with empty diff (no changes)
|
||||||
|
* 1 = Error
|
||||||
|
* 2 = Succeeded with non-empty diff (changes present)
|
||||||
|
|
||||||
* `-input=true` - Ask for input for variables if not directly set.
|
* `-input=true` - Ask for input for variables if not directly set.
|
||||||
|
|
||||||
* `-module-depth=n` - Specifies the depth of modules to show in the output.
|
* `-module-depth=n` - Specifies the depth of modules to show in the output.
|
||||||
|
@ -45,6 +52,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||||
|
|
||||||
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||||
|
|
||||||
|
* `-target=resource` - A [Resource
|
||||||
|
Address](/docs/internals/resource-addressing.html) to target. Operation will
|
||||||
|
be limited to this resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||||
* `-state-out=path` - Path to write updated state file. By default, the
|
* `-state-out=path` - Path to write updated state file. By default, the
|
||||||
`-state` path will be used.
|
`-state` path will be used.
|
||||||
|
|
||||||
|
* `-target=resource` - A [Resource
|
||||||
|
Address](/docs/internals/resource-addressing.html) to target. Operation will
|
||||||
|
be limited to this resource and its dependencies. This flag can be used
|
||||||
|
multiple times.
|
||||||
|
|
||||||
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Internals: Resource Address"
|
||||||
|
sidebar_current: "docs-internals-resource-addressing"
|
||||||
|
description: |-
|
||||||
|
Resource addressing is used to target specific resources in a larger
|
||||||
|
infrastructure.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource Addressing
|
||||||
|
|
||||||
|
A __Resource Address__ is a string that references a specific resource in a
|
||||||
|
larger infrastructure. The syntax of a resource address is:
|
||||||
|
|
||||||
|
```
|
||||||
|
<resource_type>.<resource_name>[optional fields]
|
||||||
|
```
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
* `resource_type` - Type of the resource being addressed.
|
||||||
|
* `resource_name` - User-defined name of the resource.
|
||||||
|
|
||||||
|
Optional fields may include:
|
||||||
|
|
||||||
|
* `[N]` - where `N` is a `0`-based index into a resource with multiple
|
||||||
|
instances specified by the `count` meta-parameter. Omitting an index when
|
||||||
|
addressing a resource where `count > 1` means that the address references
|
||||||
|
all instances.
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Given a Terraform config that includes:
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "aws_instance" "web" {
|
||||||
|
# ...
|
||||||
|
count = 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An address like this:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
aws_instance.web[3]
|
||||||
|
```
|
||||||
|
|
||||||
|
Refers to only the last instance in the config, and an address like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
aws_instance.web
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Refers to all four "web" instances.
|
|
@ -62,6 +62,9 @@ The following arguments are supported:
|
||||||
* `db_subnet_group_name` - (Optional) Name of DB subnet group
|
* `db_subnet_group_name` - (Optional) Name of DB subnet group
|
||||||
* `parameter_group_name` - (Optional) Name of the DB parameter group to associate.
|
* `parameter_group_name` - (Optional) Name of the DB parameter group to associate.
|
||||||
* `storage_encrypted` - (Optional) Specifies whether the DB instance is encrypted. The Default is `false` if not specified.
|
* `storage_encrypted` - (Optional) Specifies whether the DB instance is encrypted. The Default is `false` if not specified.
|
||||||
|
* `apply_immediately` - (Optional) Specifies whether any database modifications
|
||||||
|
are applied immediately, or during the next maintenance window. Default is
|
||||||
|
`False`. See [Amazon RDS Documentation for more for more information.](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html)
|
||||||
|
|
||||||
## Attributes Reference
|
## Attributes Reference
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@ resource "heroku_app" "default" {
|
||||||
|
|
||||||
The following arguments are supported:
|
The following arguments are supported:
|
||||||
|
|
||||||
* `api_key` - (Required) Heroku API token
|
* `api_key` - (Required) Heroku API token. It must be provided, but it can also
|
||||||
* `email` - (Required) Email to be notified by Heroku
|
be sourced from the `HEROKU_API_KEY` environment variable.
|
||||||
|
* `email` - (Required) Email to be notified by Heroku. It must be provided, but
|
||||||
|
it can also be sourced from the `HEROKU_EMAIL` environment variable.
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,12 @@ resource "heroku_app" "default" {
|
||||||
name = "test-app"
|
name = "test-app"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Create a database, and configure the app to use it
|
||||||
|
resource "heroku_addon" "database" {
|
||||||
|
app = "${heroku_app.default.name}"
|
||||||
|
plan = "heroku-postgresql:hobby-basic"
|
||||||
|
}
|
||||||
|
|
||||||
# Add a web-hook addon for the app
|
# Add a web-hook addon for the app
|
||||||
resource "heroku_addon" "webhook" {
|
resource "heroku_addon" "webhook" {
|
||||||
app = "${heroku_app.default.name}"
|
app = "${heroku_app.default.name}"
|
||||||
|
|
|
@ -17,6 +17,7 @@ create and manage applications on Heroku.
|
||||||
# Create a new Heroku app
|
# Create a new Heroku app
|
||||||
resource "heroku_app" "default" {
|
resource "heroku_app" "default" {
|
||||||
name = "my-cool-app"
|
name = "my-cool-app"
|
||||||
|
region = "us"
|
||||||
|
|
||||||
config_vars {
|
config_vars {
|
||||||
FOOBAR = "baz"
|
FOOBAR = "baz"
|
||||||
|
|
|
@ -219,6 +219,10 @@
|
||||||
<li<%= sidebar_current("docs-internals-lifecycle") %>>
|
<li<%= sidebar_current("docs-internals-lifecycle") %>>
|
||||||
<a href="/docs/internals/lifecycle.html">Resource Lifecycle</a>
|
<a href="/docs/internals/lifecycle.html">Resource Lifecycle</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-internals-resource-addressing") %>>
|
||||||
|
<a href="/docs/internals/resource-addressing.html">Resource Addressing</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue