From 5c5a2bc0b7b1223fd38ffe5e93ff0874bf35d146 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Mon, 21 Sep 2026 15:15:27 +1000 Subject: [PATCH] fix: omit the user data attribute value when an instance has none (#1097) DescribeInstanceAttribute always returned a Value for userData, an empty string when the instance was launched without any. AWS returns the attribute with no Value at all in that case. The Terraform provider reads user_data only when Value is non-nil, so an empty string made it record the hash of the empty string in state and propose a change on every plan for an instance whose configuration names no user data. --- .../handlers/ec2/instance/service_impl.go | 10 +++--- .../ec2/instance/service_impl_test.go | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/spinifex/handlers/ec2/instance/service_impl.go b/spinifex/handlers/ec2/instance/service_impl.go index 4ad7e0ea2..d4e8c6f2c 100644 --- a/spinifex/handlers/ec2/instance/service_impl.go +++ b/spinifex/handlers/ec2/instance/service_impl.go @@ -3092,12 +3092,12 @@ func (s *InstanceServiceImpl) DescribeInstanceAttribute(ctx context.Context, inp case ec2.InstanceAttributeNameUserData: // AWS returns user-data base64-encoded; RunInstancesInput.UserData is the - // canonical base64 store, set at launch and kept in sync by Modify. - var val string - if instance.RunInstancesInput != nil && instance.RunInstancesInput.UserData != nil { - val = *instance.RunInstancesInput.UserData + // canonical base64 store, set at launch and kept in sync by Modify. An + // instance with no user data carries no Value at all, not an empty one. + output.UserData = &ec2.AttributeValue{} + if instance.RunInstancesInput != nil && aws.StringValue(instance.RunInstancesInput.UserData) != "" { + output.UserData.Value = aws.String(*instance.RunInstancesInput.UserData) } - output.UserData = &ec2.AttributeValue{Value: &val} case ec2.InstanceAttributeNameDisableApiTermination: // Read under the manager lock so we serialise with a concurrent diff --git a/spinifex/handlers/ec2/instance/service_impl_test.go b/spinifex/handlers/ec2/instance/service_impl_test.go index a845ce5b4..68f3a616c 100644 --- a/spinifex/handlers/ec2/instance/service_impl_test.go +++ b/spinifex/handlers/ec2/instance/service_impl_test.go @@ -4822,3 +4822,35 @@ func TestPrepareRunInstances_PreCreatedENIAttachErrorSurfaced(t *testing.T) { assert.Equal(t, 1, eni.attachCalls) assert.Equal(t, 0, eni.createCalls) } + +// TestDescribeInstanceAttribute_UserDataUnsetCarriesNoValue pins the shape AWS +// returns for an instance launched without user data: the attribute is present +// and its Value is absent. The Terraform provider reads user_data only when +// Value is non-nil, so an empty string makes it record the hash of "" and +// propose a change on every plan. +func TestDescribeInstanceAttribute_UserDataUnsetCarriesNoValue(t *testing.T) { + const id = "i-userdata-unset" + const owner = "000000000000" + + for _, tc := range []struct { + name string + input *ec2.RunInstancesInput + }{ + {name: "no RunInstancesInput", input: nil}, + {name: "nil UserData", input: &ec2.RunInstancesInput{}}, + {name: "empty UserData", input: &ec2.RunInstancesInput{UserData: aws.String("")}}, + } { + t.Run(tc.name, func(t *testing.T) { + v := &vm.VM{ID: id, AccountID: owner, RunInstancesInput: tc.input} + svc := &InstanceServiceImpl{vmMgr: mgrWith(map[string]*vm.VM{id: v})} + + out, err := svc.DescribeInstanceAttribute(context.Background(), &ec2.DescribeInstanceAttributeInput{ + InstanceId: aws.String(id), + Attribute: aws.String(ec2.InstanceAttributeNameUserData), + }, owner) + require.NoError(t, err) + require.NotNil(t, out.UserData, "AWS returns the attribute itself even when it holds nothing") + assert.Nil(t, out.UserData.Value, "an instance with no user data carries no Value, not an empty one") + }) + } +}