diff --git a/CHANGELOG.md b/CHANGELOG.md index 857cd7923..4e2ed6e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,18 @@ FEATURES: * **Self-variables** can be used to reference the current resource's attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033] + * **Continous state** saving during `terraform apply`. The state file is + continously updated as apply is running, meaning that the state is + less likely to become corrupt in a catastrophic case: terraform panic + or system killing Terraform. IMPROVEMENTS: * **New config function: `split`** - Split a value based on a delimiter. This is useful for faking lists as parameters to modules. + * core: The serial of the state is only updated if there is an actual + change. This will lower the amount of state changing on things + like refresh. BUG FIXES: diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 99367642b..dadb2f1b5 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -7,14 +7,13 @@ import ( "unicode" "github.com/hashicorp/terraform/helper/multierror" - "github.com/mitchellh/goamz/autoscaling" "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/elb" "github.com/mitchellh/goamz/rds" awsGo "github.com/awslabs/aws-sdk-go/aws" - awsAutoScaling "github.com/awslabs/aws-sdk-go/gen/autoscaling" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" awsRDS "github.com/awslabs/aws-sdk-go/gen/rds" "github.com/awslabs/aws-sdk-go/gen/route53" "github.com/awslabs/aws-sdk-go/gen/s3" @@ -27,15 +26,14 @@ type Config struct { } type AWSClient struct { - ec2conn *ec2.EC2 - elbconn *elb.ELB - autoscalingconn *autoscaling.AutoScaling - s3conn *s3.S3 - rdsconn *rds.Rds - r53conn *route53.Route53 - region string - awsAutoScalingconn *awsAutoScaling.AutoScaling - awsRDSconn *awsRDS.RDS + ec2conn *ec2.EC2 + elbconn *elb.ELB + autoscalingconn *autoscaling.AutoScaling + s3conn *s3.S3 + rdsconn *rds.Rds + r53conn *route53.Route53 + region string + awsRDSconn *awsRDS.RDS } // Client configures and returns a fully initailized AWSClient @@ -69,7 +67,7 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing ELB connection") client.elbconn = elb.New(auth, region) log.Println("[INFO] Initializing AutoScaling connection") - client.autoscalingconn = autoscaling.New(auth, region) + client.autoscalingconn = autoscaling.New(creds, c.Region, nil) log.Println("[INFO] Initializing S3 connection") client.s3conn = s3.New(creds, c.Region, nil) log.Println("[INFO] Initializing RDS connection") @@ -80,8 +78,6 @@ func (c *Config) Client() (interface{}, error) { // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html log.Println("[INFO] Initializing Route53 connection") client.r53conn = route53.New(creds, "us-east-1", nil) - log.Println("[INFO] Initializing AWS Go AutoScaling connection") - client.awsAutoScalingconn = awsAutoScaling.New(creds, c.Region, nil) log.Println("[INFO] Initializing AWS Go RDS connection") client.awsRDSconn = awsRDS.New(creds, c.Region, nil) } diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 2a285cf18..ebe9ac72d 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -123,7 +123,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { } func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn var autoScalingGroupOpts autoscaling.CreateAutoScalingGroupType autoScalingGroupOpts.AutoScalingGroupName = aws.String(d.Get("name").(string)) @@ -199,7 +199,7 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e } func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn opts := autoscaling.UpdateAutoScalingGroupType{ AutoScalingGroupName: aws.String(d.Id()), @@ -232,7 +232,7 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn // Read the autoscaling group first. If it doesn't exist, we're done. // We need the group in order to check if there are instances attached. @@ -276,7 +276,7 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) func getAwsAutoscalingGroup( d *schema.ResourceData, meta interface{}) (*autoscaling.AutoScalingGroup, error) { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn describeOpts := autoscaling.AutoScalingGroupNamesType{ AutoScalingGroupNames: []string{d.Id()}, @@ -307,7 +307,7 @@ func getAwsAutoscalingGroup( } func resourceAwsAutoscalingGroupDrain(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn // First, set the capacity to zero so the group will drain log.Printf("[DEBUG] Reducing autoscaling group capacity to zero") diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index 531d51843..1d87f1788 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -4,9 +4,10 @@ import ( "fmt" "testing" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/autoscaling" ) func TestAccAWSAutoScalingGroup_basic(t *testing.T) { @@ -51,8 +52,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.new", &lc), resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "desired_capacity", "5"), - resource.TestCheckResourceAttrPtr( - "aws_autoscaling_group.bar", "launch_configuration", &lc.Name), + testLaunchConfigurationName("aws_autoscaling_group.bar", &lc), ), }, }, @@ -87,19 +87,19 @@ func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error { // Try to find the Group describeGroups, err := conn.DescribeAutoScalingGroups( - &autoscaling.DescribeAutoScalingGroups{ - Names: []string{rs.Primary.ID}, + &autoscaling.AutoScalingGroupNamesType{ + AutoScalingGroupNames: []string{rs.Primary.ID}, }) if err == nil { if len(describeGroups.AutoScalingGroups) != 0 && - describeGroups.AutoScalingGroups[0].Name == rs.Primary.ID { + *describeGroups.AutoScalingGroups[0].AutoScalingGroupName == rs.Primary.ID { return fmt.Errorf("AutoScaling Group still exists") } } // Verify the error - ec2err, ok := err.(*autoscaling.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -117,32 +117,32 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro return fmt.Errorf("Bad availability_zones: %s", group.AvailabilityZones[0]) } - if group.Name != "foobar3-terraform-test" { - return fmt.Errorf("Bad name: %s", group.Name) + if *group.AutoScalingGroupName != "foobar3-terraform-test" { + return fmt.Errorf("Bad name: %s", *group.AutoScalingGroupName) } - if group.MaxSize != 5 { - return fmt.Errorf("Bad max_size: %d", group.MaxSize) + if *group.MaxSize != 5 { + return fmt.Errorf("Bad max_size: %d", *group.MaxSize) } - if group.MinSize != 2 { - return fmt.Errorf("Bad max_size: %d", group.MinSize) + if *group.MinSize != 2 { + return fmt.Errorf("Bad max_size: %d", *group.MinSize) } - if group.HealthCheckType != "ELB" { - return fmt.Errorf("Bad health_check_type: %s", group.HealthCheckType) + if *group.HealthCheckType != "ELB" { + return fmt.Errorf("Bad health_check_type: %s", *group.HealthCheckType) } - if group.HealthCheckGracePeriod != 300 { - return fmt.Errorf("Bad health_check_grace_period: %d", group.HealthCheckGracePeriod) + if *group.HealthCheckGracePeriod != 300 { + return fmt.Errorf("Bad health_check_grace_period: %d", *group.HealthCheckGracePeriod) } - if group.DesiredCapacity != 4 { - return fmt.Errorf("Bad desired_capacity: %d", group.DesiredCapacity) + if *group.DesiredCapacity != 4 { + return fmt.Errorf("Bad desired_capacity: %d", *group.DesiredCapacity) } - if group.LaunchConfigurationName == "" { - return fmt.Errorf("Bad launch configuration name: %s", group.LaunchConfigurationName) + if *group.LaunchConfigurationName == "" { + return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName) } return nil @@ -172,8 +172,8 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal conn := testAccProvider.Meta().(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeAutoScalingGroups{ - Names: []string{rs.Primary.ID}, + describeOpts := autoscaling.AutoScalingGroupNamesType{ + AutoScalingGroupNames: []string{rs.Primary.ID}, } describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts) @@ -182,7 +182,7 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal } if len(describeGroups.AutoScalingGroups) != 1 || - describeGroups.AutoScalingGroups[0].Name != rs.Primary.ID { + *describeGroups.AutoScalingGroups[0].AutoScalingGroupName != rs.Primary.ID { return fmt.Errorf("AutoScaling Group not found") } @@ -192,6 +192,21 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal } } +func testLaunchConfigurationName(n string, lc *autoscaling.LaunchConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if *lc.LaunchConfigurationName != rs.Primary.Attributes["launch_configuration"] { + return fmt.Errorf("Launch configuration names do not match") + } + + return nil + } +} + const testAccAWSAutoScalingGroupConfig = ` resource "aws_launch_configuration" "foobar" { name = "foobarautoscaling-terraform-test" diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index 1092956f7..021e9417c 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -2,15 +2,17 @@ package aws import ( "crypto/sha1" + "encoding/base64" "encoding/hex" "fmt" "log" "time" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/autoscaling" ) func resourceAwsLaunchConfiguration() *schema.Resource { @@ -94,15 +96,26 @@ func resourceAwsLaunchConfiguration() *schema.Resource { func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - var createLaunchConfigurationOpts autoscaling.CreateLaunchConfiguration - createLaunchConfigurationOpts.Name = d.Get("name").(string) - createLaunchConfigurationOpts.IamInstanceProfile = d.Get("iam_instance_profile").(string) - createLaunchConfigurationOpts.ImageId = d.Get("image_id").(string) - createLaunchConfigurationOpts.InstanceType = d.Get("instance_type").(string) - createLaunchConfigurationOpts.KeyName = d.Get("key_name").(string) - createLaunchConfigurationOpts.UserData = d.Get("user_data").(string) - createLaunchConfigurationOpts.AssociatePublicIpAddress = d.Get("associate_public_ip_address").(bool) - createLaunchConfigurationOpts.SpotPrice = d.Get("spot_price").(string) + var createLaunchConfigurationOpts autoscaling.CreateLaunchConfigurationType + createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(d.Get("name").(string)) + createLaunchConfigurationOpts.ImageID = aws.String(d.Get("image_id").(string)) + createLaunchConfigurationOpts.InstanceType = aws.String(d.Get("instance_type").(string)) + + if v, ok := d.GetOk("user_data"); ok { + createLaunchConfigurationOpts.UserData = aws.String(base64.StdEncoding.EncodeToString([]byte(v.(string)))) + } + if v, ok := d.GetOk("associate_public_ip_address"); ok { + createLaunchConfigurationOpts.AssociatePublicIPAddress = aws.Boolean(v.(bool)) + } + if v, ok := d.GetOk("iam_instance_profile"); ok { + createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string)) + } + if v, ok := d.GetOk("key_name"); ok { + createLaunchConfigurationOpts.KeyName = aws.String(v.(string)) + } + if v, ok := d.GetOk("spot_price"); ok { + createLaunchConfigurationOpts.SpotPrice = aws.String(v.(string)) + } if v, ok := d.GetOk("security_groups"); ok { createLaunchConfigurationOpts.SecurityGroups = expandStringList( @@ -110,7 +123,7 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface } log.Printf("[DEBUG] autoscaling create launch configuration: %#v", createLaunchConfigurationOpts) - _, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) + err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) if err != nil { return fmt.Errorf("Error creating launch configuration: %s", err) } @@ -128,8 +141,8 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeLaunchConfigurations{ - Names: []string{d.Id()}, + describeOpts := autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{d.Id()}, } log.Printf("[DEBUG] launch configuration describe configuration: %#v", describeOpts) @@ -143,7 +156,7 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{} } // Verify AWS returned our launch configuration - if describConfs.LaunchConfigurations[0].Name != d.Id() { + if *describConfs.LaunchConfigurations[0].LaunchConfigurationName != d.Id() { return fmt.Errorf( "Unable to find launch configuration: %#v", describConfs.LaunchConfigurations) @@ -151,14 +164,28 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{} lc := describConfs.LaunchConfigurations[0] - d.Set("key_name", lc.KeyName) - d.Set("iam_instance_profile", lc.IamInstanceProfile) - d.Set("image_id", lc.ImageId) - d.Set("instance_type", lc.InstanceType) - d.Set("name", lc.Name) - d.Set("security_groups", lc.SecurityGroups) - d.Set("spot_price", lc.SpotPrice) + d.Set("key_name", *lc.KeyName) + d.Set("image_id", *lc.ImageID) + d.Set("instance_type", *lc.InstanceType) + d.Set("name", *lc.LaunchConfigurationName) + if lc.IAMInstanceProfile != nil { + d.Set("iam_instance_profile", *lc.IAMInstanceProfile) + } else { + d.Set("iam_instance_profile", nil) + } + + if lc.SpotPrice != nil { + d.Set("spot_price", *lc.SpotPrice) + } else { + d.Set("spot_price", nil) + } + + if lc.SecurityGroups != nil { + d.Set("security_groups", lc.SecurityGroups) + } else { + d.Set("security_groups", nil) + } return nil } @@ -166,10 +193,10 @@ func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface autoscalingconn := meta.(*AWSClient).autoscalingconn log.Printf("[DEBUG] Launch Configuration destroy: %v", d.Id()) - _, err := autoscalingconn.DeleteLaunchConfiguration( - &autoscaling.DeleteLaunchConfiguration{Name: d.Id()}) + err := autoscalingconn.DeleteLaunchConfiguration( + &autoscaling.LaunchConfigurationNameType{LaunchConfigurationName: aws.String(d.Id())}) if err != nil { - autoscalingerr, ok := err.(*autoscaling.Error) + autoscalingerr, ok := err.(aws.APIError) if ok && autoscalingerr.Code == "InvalidConfiguration.NotFound" { return nil } diff --git a/builtin/providers/aws/resource_aws_launch_configuration_test.go b/builtin/providers/aws/resource_aws_launch_configuration_test.go index 32036af59..711932f41 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration_test.go +++ b/builtin/providers/aws/resource_aws_launch_configuration_test.go @@ -4,9 +4,10 @@ import ( "fmt" "testing" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/autoscaling" ) func TestAccAWSLaunchConfiguration(t *testing.T) { @@ -57,19 +58,19 @@ func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error { } describe, err := conn.DescribeLaunchConfigurations( - &autoscaling.DescribeLaunchConfigurations{ - Names: []string{rs.Primary.ID}, + &autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{rs.Primary.ID}, }) if err == nil { if len(describe.LaunchConfigurations) != 0 && - describe.LaunchConfigurations[0].Name == rs.Primary.ID { + *describe.LaunchConfigurations[0].LaunchConfigurationName == rs.Primary.ID { return fmt.Errorf("Launch Configuration still exists") } } // Verify the error - providerErr, ok := err.(*autoscaling.Error) + providerErr, ok := err.(aws.APIError) if !ok { return err } @@ -83,16 +84,16 @@ func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error { func testAccCheckAWSLaunchConfigurationAttributes(conf *autoscaling.LaunchConfiguration) resource.TestCheckFunc { return func(s *terraform.State) error { - if conf.ImageId != "ami-21f78e11" { - return fmt.Errorf("Bad image_id: %s", conf.ImageId) + if *conf.ImageID != "ami-21f78e11" { + return fmt.Errorf("Bad image_id: %s", *conf.ImageID) } - if conf.Name != "foobar-terraform-test" { - return fmt.Errorf("Bad name: %s", conf.Name) + if *conf.LaunchConfigurationName != "foobar-terraform-test" { + return fmt.Errorf("Bad name: %s", *conf.LaunchConfigurationName) } - if conf.InstanceType != "t1.micro" { - return fmt.Errorf("Bad instance_type: %s", conf.InstanceType) + if *conf.InstanceType != "t1.micro" { + return fmt.Errorf("Bad instance_type: %s", *conf.InstanceType) } return nil @@ -112,8 +113,8 @@ func testAccCheckAWSLaunchConfigurationExists(n string, res *autoscaling.LaunchC conn := testAccProvider.Meta().(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeLaunchConfigurations{ - Names: []string{rs.Primary.ID}, + describeOpts := autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{rs.Primary.ID}, } describe, err := conn.DescribeLaunchConfigurations(&describeOpts) @@ -122,7 +123,7 @@ func testAccCheckAWSLaunchConfigurationExists(n string, res *autoscaling.LaunchC } if len(describe.LaunchConfigurations) != 1 || - describe.LaunchConfigurations[0].Name != rs.Primary.ID { + *describe.LaunchConfigurations[0].LaunchConfigurationName != rs.Primary.ID { return fmt.Errorf("Launch Configuration Group not found") } diff --git a/command/apply.go b/command/apply.go index a568b0431..d46b71679 100644 --- a/command/apply.go +++ b/command/apply.go @@ -68,7 +68,8 @@ func (c *ApplyCommand) Run(args []string) int { // Prepare the extra hooks to count resources countHook := new(CountHook) - c.Meta.extraHooks = []terraform.Hook{countHook} + stateHook := new(StateHook) + c.Meta.extraHooks = []terraform.Hook{countHook, stateHook} if !c.Destroy && maybeInit { // Do a detect to determine if we need to do an init + apply. @@ -151,6 +152,18 @@ func (c *ApplyCommand) Run(args []string) int { } } + // Setup the state hook for continous state updates + { + state, err := c.State() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error reading state: %s", err)) + return 1 + } + + stateHook.State = state + } + // Start the apply in a goroutine so that we can be interrupted. var state *terraform.State var applyErr error diff --git a/command/hook_state.go b/command/hook_state.go new file mode 100644 index 000000000..ab5c47a11 --- /dev/null +++ b/command/hook_state.go @@ -0,0 +1,33 @@ +package command + +import ( + "sync" + + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +// StateHook is a hook that continuously updates the state by calling +// WriteState on a state.State. +type StateHook struct { + terraform.NilHook + sync.Mutex + + State state.State +} + +func (h *StateHook) PostStateUpdate( + s *terraform.State) (terraform.HookAction, error) { + h.Lock() + defer h.Unlock() + + if h.State != nil { + // Write the new state + if err := h.State.WriteState(s); err != nil { + return terraform.HookActionHalt, err + } + } + + // Continue forth + return terraform.HookActionContinue, nil +} diff --git a/command/hook_state_test.go b/command/hook_state_test.go new file mode 100644 index 000000000..0d0fd7927 --- /dev/null +++ b/command/hook_state_test.go @@ -0,0 +1,29 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +func TestStateHook_impl(t *testing.T) { + var _ terraform.Hook = new(StateHook) +} + +func TestStateHook(t *testing.T) { + is := &state.InmemState{} + var hook terraform.Hook = &StateHook{State: is} + + s := state.TestStateInitial() + action, err := hook.PostStateUpdate(s) + if err != nil { + t.Fatalf("err: %s", err) + } + if action != terraform.HookActionContinue { + t.Fatalf("bad: %v", action) + } + if !is.State().Equal(s) { + t.Fatalf("bad state: %#v", is.State()) + } +} diff --git a/state/cache.go b/state/cache.go index 3322ab5ac..a20eb4a06 100644 --- a/state/cache.go +++ b/state/cache.go @@ -19,7 +19,7 @@ type CacheState struct { // StateReader impl. func (s *CacheState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // WriteState will write and persist the state to the cache. @@ -104,6 +104,8 @@ func (s *CacheState) RefreshState() error { s.refreshResult = CacheRefreshNoop return err } + + cached = durable } s.state = cached diff --git a/state/inmem.go b/state/inmem.go index 82385a6df..ff8daab8f 100644 --- a/state/inmem.go +++ b/state/inmem.go @@ -10,7 +10,7 @@ type InmemState struct { } func (s *InmemState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } func (s *InmemState) RefreshState() error { @@ -18,6 +18,7 @@ func (s *InmemState) RefreshState() error { } func (s *InmemState) WriteState(state *terraform.State) error { + state.IncrementSerialMaybe(s.state) s.state = state return nil } diff --git a/state/local.go b/state/local.go index 1840cae1a..02afb1ed7 100644 --- a/state/local.go +++ b/state/local.go @@ -15,18 +15,20 @@ type LocalState struct { Path string PathOut string - state *terraform.State - written bool + state *terraform.State + readState *terraform.State + written bool } // SetState will force a specific state in-memory for this local state. func (s *LocalState) SetState(state *terraform.State) { s.state = state + s.readState = state } // StateReader impl. func (s *LocalState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // WriteState for LocalState always persists the state as well. @@ -61,6 +63,9 @@ func (s *LocalState) WriteState(state *terraform.State) error { } defer f.Close() + s.state.IncrementSerialMaybe(s.readState) + s.readState = s.state + if err := terraform.WriteState(s.state, f); err != nil { return err } @@ -105,5 +110,6 @@ func (s *LocalState) RefreshState() error { } s.state = state + s.readState = state return nil } diff --git a/state/remote/state.go b/state/remote/state.go index 5137744ec..e679b5d73 100644 --- a/state/remote/state.go +++ b/state/remote/state.go @@ -13,12 +13,12 @@ import ( type State struct { Client Client - state *terraform.State + state, readState *terraform.State } // StateReader impl. func (s *State) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // StateWriter impl. @@ -43,11 +43,14 @@ func (s *State) RefreshState() error { } s.state = state + s.readState = state return nil } // StatePersister impl. func (s *State) PersistState() error { + s.state.IncrementSerialMaybe(s.readState) + var buf bytes.Buffer if err := terraform.WriteState(s.state, &buf); err != nil { return err diff --git a/state/remote/state_test.go b/state/remote/state_test.go index 08b51439b..487891667 100644 --- a/state/remote/state_test.go +++ b/state/remote/state_test.go @@ -7,8 +7,11 @@ import ( ) func TestState(t *testing.T) { - s := &State{Client: new(InmemClient)} - s.WriteState(state.TestStateInitial()) + s := &State{ + Client: new(InmemClient), + state: state.TestStateInitial(), + readState: state.TestStateInitial(), + } if err := s.PersistState(); err != nil { t.Fatalf("err: %s", err) } diff --git a/state/testing.go b/state/testing.go index 3ada81e40..6a4a88ad0 100644 --- a/state/testing.go +++ b/state/testing.go @@ -28,9 +28,7 @@ func TestState(t *testing.T, s interface{}) { current := TestStateInitial() // Check that the initial state is correct - state := reader.State() - current.Serial = state.Serial - if !reflect.DeepEqual(state, current) { + if state := reader.State(); !current.Equal(state) { t.Fatalf("not initial: %#v\n\n%#v", state, current) } @@ -47,7 +45,7 @@ func TestState(t *testing.T, s interface{}) { t.Fatalf("err: %s", err) } - if actual := reader.State(); !reflect.DeepEqual(actual, current) { + if actual := reader.State(); !actual.Equal(current) { t.Fatalf("bad: %#v\n\n%#v", actual, current) } } @@ -67,11 +65,55 @@ func TestState(t *testing.T, s interface{}) { // Just set the serials the same... Then compare. actual := reader.State() - actual.Serial = current.Serial - if !reflect.DeepEqual(actual, current) { + if !actual.Equal(current) { t.Fatalf("bad: %#v\n\n%#v", actual, current) } } + + // If we can write and persist then verify that the serial + // is only implemented on change. + writer, writeOk := s.(StateWriter) + persister, persistOk := s.(StatePersister) + if writeOk && persistOk { + // Same serial + serial := current.Serial + if err := writer.WriteState(current); err != nil { + t.Fatalf("err: %s", err) + } + if err := persister.PersistState(); err != nil { + t.Fatalf("err: %s", err) + } + + if reader.State().Serial != serial { + t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial) + } + + // Change the serial + currentCopy := *current + current = ¤tCopy + current.Modules = []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root", "somewhere"}, + Outputs: map[string]string{"serialCheck": "true"}, + }, + } + if err := writer.WriteState(current); err != nil { + t.Fatalf("err: %s", err) + } + if err := persister.PersistState(); err != nil { + t.Fatalf("err: %s", err) + } + + if reader.State().Serial <= serial { + t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial) + } + + // Check that State() returns a copy + reader.State().Serial++ + if reflect.DeepEqual(reader.State(), current) { + t.Fatal("State() should return a copy") + } + } } // TestStateInitial is the initial state that a State should have diff --git a/terraform/context.go b/terraform/context.go index 97abe573c..e2db6e6f9 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -224,7 +224,7 @@ func (c *Context) Apply() (*State, error) { defer c.releaseRun(v) // Copy our own state - c.state = c.state.deepcopy() + c.state = c.state.DeepCopy() // Do the walk _, err := c.walk(walkApply) @@ -264,7 +264,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { c.state = &State{} c.state.init() } else { - c.state = old.deepcopy() + c.state = old.DeepCopy() } defer func() { c.state = old @@ -299,7 +299,7 @@ func (c *Context) Refresh() (*State, error) { defer c.releaseRun(v) // Copy our own state - c.state = c.state.deepcopy() + c.state = c.state.DeepCopy() // Do the walk if _, err := c.walk(walkRefresh); err != nil { diff --git a/terraform/context_test.go b/terraform/context_test.go index e4359685f..5ee9a8c71 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -4454,6 +4454,9 @@ func TestContext2Apply_hook(t *testing.T) { if !h.PostApplyCalled { t.Fatal("should be called") } + if !h.PostStateUpdateCalled { + t.Fatalf("should call post state update") + } } func TestContext2Apply_idAttr(t *testing.T) { diff --git a/terraform/eval_context.go b/terraform/eval_context.go index e1d4eef51..120cf71e7 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -70,155 +70,3 @@ type EvalContext interface { // be used to modify that state. State() (*State, *sync.RWMutex) } - -// MockEvalContext is a mock version of EvalContext that can be used -// for tests. -type MockEvalContext struct { - HookCalled bool - HookError error - - InputCalled bool - InputInput UIInput - - InitProviderCalled bool - InitProviderName string - InitProviderProvider ResourceProvider - InitProviderError error - - ProviderCalled bool - ProviderName string - ProviderProvider ResourceProvider - - ProviderInputCalled bool - ProviderInputName string - ProviderInputConfig map[string]interface{} - - SetProviderInputCalled bool - SetProviderInputName string - SetProviderInputConfig map[string]interface{} - - ConfigureProviderCalled bool - ConfigureProviderName string - ConfigureProviderConfig *ResourceConfig - ConfigureProviderError error - - ParentProviderConfigCalled bool - ParentProviderConfigName string - ParentProviderConfigConfig *ResourceConfig - - InitProvisionerCalled bool - InitProvisionerName string - InitProvisionerProvisioner ResourceProvisioner - InitProvisionerError error - - ProvisionerCalled bool - ProvisionerName string - ProvisionerProvisioner ResourceProvisioner - - InterpolateCalled bool - InterpolateConfig *config.RawConfig - InterpolateResource *Resource - InterpolateConfigResult *ResourceConfig - InterpolateError error - - PathCalled bool - PathPath []string - - SetVariablesCalled bool - SetVariablesVariables map[string]string - - DiffCalled bool - DiffDiff *Diff - DiffLock *sync.RWMutex - - StateCalled bool - StateState *State - StateLock *sync.RWMutex -} - -func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { - c.HookCalled = true - return c.HookError -} - -func (c *MockEvalContext) Input() UIInput { - c.InputCalled = true - return c.InputInput -} - -func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { - c.InitProviderCalled = true - c.InitProviderName = n - return c.InitProviderProvider, c.InitProviderError -} - -func (c *MockEvalContext) Provider(n string) ResourceProvider { - c.ProviderCalled = true - c.ProviderName = n - return c.ProviderProvider -} - -func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error { - c.ConfigureProviderCalled = true - c.ConfigureProviderName = n - c.ConfigureProviderConfig = cfg - return c.ConfigureProviderError -} - -func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig { - c.ParentProviderConfigCalled = true - c.ParentProviderConfigName = n - return c.ParentProviderConfigConfig -} - -func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} { - c.ProviderInputCalled = true - c.ProviderInputName = n - return c.ProviderInputConfig -} - -func (c *MockEvalContext) SetProviderInput(n string, cfg map[string]interface{}) { - c.SetProviderInputCalled = true - c.SetProviderInputName = n - c.SetProviderInputConfig = cfg -} - -func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { - c.InitProvisionerCalled = true - c.InitProvisionerName = n - return c.InitProvisionerProvisioner, c.InitProvisionerError -} - -func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { - c.ProvisionerCalled = true - c.ProvisionerName = n - return c.ProvisionerProvisioner -} - -func (c *MockEvalContext) Interpolate( - config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { - c.InterpolateCalled = true - c.InterpolateConfig = config - c.InterpolateResource = resource - return c.InterpolateConfigResult, c.InterpolateError -} - -func (c *MockEvalContext) Path() []string { - c.PathCalled = true - return c.PathPath -} - -func (c *MockEvalContext) SetVariables(vs map[string]string) { - c.SetVariablesCalled = true - c.SetVariablesVariables = vs -} - -func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { - c.DiffCalled = true - return c.DiffDiff, c.DiffLock -} - -func (c *MockEvalContext) State() (*State, *sync.RWMutex) { - c.StateCalled = true - return c.StateState, c.StateLock -} diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go new file mode 100644 index 000000000..3190f680a --- /dev/null +++ b/terraform/eval_context_mock.go @@ -0,0 +1,166 @@ +package terraform + +import ( + "sync" + + "github.com/hashicorp/terraform/config" +) + +// MockEvalContext is a mock version of EvalContext that can be used +// for tests. +type MockEvalContext struct { + HookCalled bool + HookHook Hook + HookError error + + InputCalled bool + InputInput UIInput + + InitProviderCalled bool + InitProviderName string + InitProviderProvider ResourceProvider + InitProviderError error + + ProviderCalled bool + ProviderName string + ProviderProvider ResourceProvider + + ProviderInputCalled bool + ProviderInputName string + ProviderInputConfig map[string]interface{} + + SetProviderInputCalled bool + SetProviderInputName string + SetProviderInputConfig map[string]interface{} + + ConfigureProviderCalled bool + ConfigureProviderName string + ConfigureProviderConfig *ResourceConfig + ConfigureProviderError error + + ParentProviderConfigCalled bool + ParentProviderConfigName string + ParentProviderConfigConfig *ResourceConfig + + InitProvisionerCalled bool + InitProvisionerName string + InitProvisionerProvisioner ResourceProvisioner + InitProvisionerError error + + ProvisionerCalled bool + ProvisionerName string + ProvisionerProvisioner ResourceProvisioner + + InterpolateCalled bool + InterpolateConfig *config.RawConfig + InterpolateResource *Resource + InterpolateConfigResult *ResourceConfig + InterpolateError error + + PathCalled bool + PathPath []string + + SetVariablesCalled bool + SetVariablesVariables map[string]string + + DiffCalled bool + DiffDiff *Diff + DiffLock *sync.RWMutex + + StateCalled bool + StateState *State + StateLock *sync.RWMutex +} + +func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { + c.HookCalled = true + if c.HookHook != nil { + if _, err := fn(c.HookHook); err != nil { + return err + } + } + + return c.HookError +} + +func (c *MockEvalContext) Input() UIInput { + c.InputCalled = true + return c.InputInput +} + +func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { + c.InitProviderCalled = true + c.InitProviderName = n + return c.InitProviderProvider, c.InitProviderError +} + +func (c *MockEvalContext) Provider(n string) ResourceProvider { + c.ProviderCalled = true + c.ProviderName = n + return c.ProviderProvider +} + +func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error { + c.ConfigureProviderCalled = true + c.ConfigureProviderName = n + c.ConfigureProviderConfig = cfg + return c.ConfigureProviderError +} + +func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig { + c.ParentProviderConfigCalled = true + c.ParentProviderConfigName = n + return c.ParentProviderConfigConfig +} + +func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} { + c.ProviderInputCalled = true + c.ProviderInputName = n + return c.ProviderInputConfig +} + +func (c *MockEvalContext) SetProviderInput(n string, cfg map[string]interface{}) { + c.SetProviderInputCalled = true + c.SetProviderInputName = n + c.SetProviderInputConfig = cfg +} + +func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { + c.InitProvisionerCalled = true + c.InitProvisionerName = n + return c.InitProvisionerProvisioner, c.InitProvisionerError +} + +func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { + c.ProvisionerCalled = true + c.ProvisionerName = n + return c.ProvisionerProvisioner +} + +func (c *MockEvalContext) Interpolate( + config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { + c.InterpolateCalled = true + c.InterpolateConfig = config + c.InterpolateResource = resource + return c.InterpolateConfigResult, c.InterpolateError +} + +func (c *MockEvalContext) Path() []string { + c.PathCalled = true + return c.PathPath +} + +func (c *MockEvalContext) SetVariables(vs map[string]string) { + c.SetVariablesCalled = true + c.SetVariablesVariables = vs +} + +func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { + c.DiffCalled = true + return c.DiffDiff, c.DiffLock +} + +func (c *MockEvalContext) State() (*State, *sync.RWMutex) { + c.StateCalled = true + return c.StateState, c.StateLock +} diff --git a/terraform/eval_state.go b/terraform/eval_state.go index bf8fec080..d584c0363 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -58,6 +58,28 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { return result, nil } +// EvalUpdateStateHook is an EvalNode implementation that calls the +// PostStateUpdate hook with the current state. +type EvalUpdateStateHook struct{} + +func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so it doesn't change while we're calling this + lock.RLock() + defer lock.RUnlock() + + // Call the hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostStateUpdate(state) + }) + if err != nil { + return nil, err + } + + return nil, nil +} + // EvalWriteState is an EvalNode implementation that reads the // InstanceState for a specific resource out of the state. type EvalWriteState struct { @@ -111,7 +133,6 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) { // Set the primary state rs.Primary = *n.State } - println(fmt.Sprintf("%#v", rs)) return nil, nil } diff --git a/terraform/eval_state_test.go b/terraform/eval_state_test.go new file mode 100644 index 000000000..0e9e98808 --- /dev/null +++ b/terraform/eval_state_test.go @@ -0,0 +1,27 @@ +package terraform + +import ( + "sync" + "testing" +) + +func TestEvalUpdateStateHook(t *testing.T) { + mockHook := new(MockHook) + + ctx := new(MockEvalContext) + ctx.HookHook = mockHook + ctx.StateState = &State{Serial: 42} + ctx.StateLock = new(sync.RWMutex) + + node := &EvalUpdateStateHook{} + if _, err := node.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !mockHook.PostStateUpdateCalled { + t.Fatal("should call PostStateUpdate") + } + if mockHook.PostStateUpdateState.Serial != 42 { + t.Fatalf("bad: %#v", mockHook.PostStateUpdateState) + } +} diff --git a/terraform/hook.go b/terraform/hook.go index e4ad42016..79e69c635 100644 --- a/terraform/hook.go +++ b/terraform/hook.go @@ -49,6 +49,9 @@ type Hook interface { // resource state is refreshed, respectively. PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) + + // PostStateUpdate is called after the state is updated. + PostStateUpdate(*State) (HookAction, error) } // NilHook is a Hook implementation that does nothing. It exists only to @@ -100,6 +103,10 @@ func (*NilHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { return HookActionContinue, nil } +func (*NilHook) PostStateUpdate(*State) (HookAction, error) { + return HookActionContinue, nil +} + // handleHook turns hook actions into panics. This lets you use the // panic/recover mechanism in Go as a flow control mechanism for hook // actions. diff --git a/terraform/hook_mock.go b/terraform/hook_mock.go index b2b6a6e6f..d30ab8f06 100644 --- a/terraform/hook_mock.go +++ b/terraform/hook_mock.go @@ -69,6 +69,11 @@ type MockHook struct { PreRefreshState *InstanceState PreRefreshReturn HookAction PreRefreshError error + + PostStateUpdateCalled bool + PostStateUpdateState *State + PostStateUpdateReturn HookAction + PostStateUpdateError error } func (h *MockHook) PreApply(n *InstanceInfo, s *InstanceState, d *InstanceDiff) (HookAction, error) { @@ -152,3 +157,9 @@ func (h *MockHook) PostRefresh(n *InstanceInfo, s *InstanceState) (HookAction, e h.PostRefreshState = s return h.PostRefreshReturn, h.PostRefreshError } + +func (h *MockHook) PostStateUpdate(s *State) (HookAction, error) { + h.PostStateUpdateCalled = true + h.PostStateUpdateState = s + return h.PostStateUpdateReturn, h.PostStateUpdateError +} diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 0dc1ad7b4..34713221e 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -53,6 +53,10 @@ func (h *stopHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error return h.hook() } +func (h *stopHook) PostStateUpdate(*State) (HookAction, error) { + return h.hook() +} + func (h *stopHook) hook() (HookAction, error) { if h.Stopped() { return HookActionHalt, nil diff --git a/terraform/state.go b/terraform/state.go index 3ef3f3afe..5b695c33c 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -161,12 +161,20 @@ func (s *State) RootModule() *ModuleState { // Equal tests if one state is equal to another. func (s *State) Equal(other *State) bool { + // If one is nil, we do a direct check + if s == nil || other == nil { + return s == other + } + // If the versions are different, they're certainly not equal if s.Version != other.Version { return false } // If any of the modules are not equal, then this state isn't equal + if len(s.Modules) != len(other.Modules) { + return false + } for _, m := range s.Modules { // This isn't very optimal currently but works. otherM := other.ModuleByPath(m.Path) @@ -183,20 +191,9 @@ func (s *State) Equal(other *State) bool { return true } -func (s *State) init() { - if s.Version == 0 { - s.Version = StateVersion - } - if len(s.Modules) == 0 { - root := &ModuleState{ - Path: rootModulePath, - } - root.init() - s.Modules = []*ModuleState{root} - } -} - -func (s *State) deepcopy() *State { +// DeepCopy performs a deep copy of the state structure and returns +// a new structure. +func (s *State) DeepCopy() *State { if s == nil { return nil } @@ -214,6 +211,27 @@ func (s *State) deepcopy() *State { return n } +// IncrementSerialMaybe increments the serial number of this state +// if it different from the other state. +func (s *State) IncrementSerialMaybe(other *State) { + if !s.Equal(other) { + s.Serial++ + } +} + +func (s *State) init() { + if s.Version == 0 { + s.Version = StateVersion + } + if len(s.Modules) == 0 { + root := &ModuleState{ + Path: rootModulePath, + } + root.init() + s.Modules = []*ModuleState{root} + } +} + // prune is used to remove any resources that are no longer required func (s *State) prune() { if s == nil { @@ -951,9 +969,6 @@ func WriteState(d *State, dst io.Writer) error { // Ensure the version is set d.Version = StateVersion - // Always increment the serial number - d.Serial++ - // Encode the data in a human-friendly way data, err := json.MarshalIndent(d, "", " ") if err != nil { diff --git a/terraform/state_test.go b/terraform/state_test.go index fc969e973..ef2aa0b59 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -116,6 +116,19 @@ func TestStateEqual(t *testing.T) { Result bool One, Two *State }{ + // Nils + { + false, + nil, + &State{Version: 2}, + }, + + { + true, + nil, + nil, + }, + // Different versions { false, @@ -159,6 +172,9 @@ func TestStateEqual(t *testing.T) { if tc.One.Equal(tc.Two) != tc.Result { t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) } + if tc.Two.Equal(tc.One) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } } } @@ -537,15 +553,6 @@ func TestReadWriteState(t *testing.T) { t.Fatalf("bad version number: %d", state.Version) } - // Verify the serial number is incremented - if state.Serial != 10 { - t.Fatalf("bad serial: %d", state.Serial) - } - - // Remove the changes or the checksum will fail - state.Version = 0 - state.Serial = 9 - // Checksum after the write chksumAfter := checksumStruct(t, state) if chksumAfter != chksum { @@ -557,10 +564,6 @@ func TestReadWriteState(t *testing.T) { t.Fatalf("err: %s", err) } - // Verify the changes came through - state.Version = StateVersion - state.Serial = 10 - // ReadState should not restore sensitive information! mod := state.RootModule() mod.Resources["foo"].Primary.Ephemeral = EphemeralState{} diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go index 780d8430b..e2a9c7dcd 100644 --- a/terraform/transform_orphan.go +++ b/terraform/transform_orphan.go @@ -262,6 +262,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode { Dependencies: n.DependentOn(), State: &state, }, + &EvalUpdateStateHook{}, }, }, }) diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 0ce4f4cd7..19c9c418a 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -400,6 +400,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { State: &state, Error: &err, }, + &EvalUpdateStateHook{}, }, }, }) diff --git a/terraform/transform_tainted.go b/terraform/transform_tainted.go index 50223a9e8..1e1b2cb31 100644 --- a/terraform/transform_tainted.go +++ b/terraform/transform_tainted.go @@ -163,6 +163,7 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode { Tainted: &tainted, TaintedIndex: n.Index, }, + &EvalUpdateStateHook{}, }, }, })