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.
This commit is contained in:
Josh Siviour
2026-09-21 15:15:27 +10:00
committed by GitHub
parent 259b737f7a
commit 5c5a2bc0b7
2 changed files with 37 additions and 5 deletions
@@ -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
@@ -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")
})
}
}