@@ -30,6 +30,33 @@ use crate::xml_responses;
3030/// supplement the eagerly-captured `ProvisionResult::attributes` with
3131/// live-state lookups via `ResourceProvisioner::get_att`. Resources not
3232/// listed here just use whatever the create handler captured.
33+ /// Build the full GetAtt attribute set for a resource: start from `base` (the
34+ /// create-time attributes, so values like `CreationTime` survive an update),
35+ /// overlay the resource's own attributes (the handler's fresh values), then
36+ /// overlay any well-known attribute the provisioner can still resolve from live
37+ /// state. Used by both create and update so a 2nd deploy's GetAtt/Outputs don't
38+ /// collapse to the update handler's subset (and resolve to literal
39+ /// `"Logical.Attr"`).
40+ fn merged_resource_attributes (
41+ provisioner : & ResourceProvisioner ,
42+ resource : & StackResource ,
43+ base : std:: collections:: BTreeMap < String , String > ,
44+ ) -> std:: collections:: BTreeMap < String , String > {
45+ let mut attr_map = base;
46+ for ( k, v) in & resource. attributes {
47+ attr_map. insert ( k. clone ( ) , v. clone ( ) ) ;
48+ }
49+ for attr in well_known_attributes_for ( & resource. resource_type ) {
50+ if attr_map. contains_key ( * attr) {
51+ continue ;
52+ }
53+ if let Some ( v) = provisioner. get_att ( resource, attr) {
54+ attr_map. insert ( ( * attr) . to_string ( ) , v) ;
55+ }
56+ }
57+ attr_map
58+ }
59+
3360fn well_known_attributes_for ( resource_type : & str ) -> & ' static [ & ' static str ] {
3461 match resource_type {
3562 "AWS::S3::Bucket" => & [
@@ -290,15 +317,11 @@ pub(crate) fn provision_stack_resources(
290317 // overlay anything the provisioner can resolve from
291318 // live state (e.g. attributes that depend on side-effects
292319 // recorded after the create handler returned).
293- let mut attr_map = stack_resource. attributes . clone ( ) ;
294- for attr in well_known_attributes_for ( & stack_resource. resource_type ) {
295- if attr_map. contains_key ( * attr) {
296- continue ;
297- }
298- if let Some ( v) = provisioner. get_att ( & stack_resource, attr) {
299- attr_map. insert ( ( * attr) . to_string ( ) , v) ;
300- }
301- }
320+ let attr_map = merged_resource_attributes (
321+ provisioner,
322+ & stack_resource,
323+ std:: collections:: BTreeMap :: new ( ) ,
324+ ) ;
302325 attributes. insert ( stack_resource. logical_id . clone ( ) , attr_map. clone ( ) ) ;
303326 // Persist the live-resolved attributes onto the stored
304327 // resource so later readers — notably resolve_template_outputs,
@@ -2778,7 +2801,7 @@ pub(crate) fn apply_resource_updates(
27782801 . cloned ( ) ;
27792802 if let Some ( existing) = existing {
27802803 match provisioner. update_resource ( & existing, & resolved_def) {
2781- Ok ( Some ( updated) ) => {
2804+ Ok ( Some ( mut updated) ) => {
27822805 changes. push ( ResourceChange {
27832806 action : ResourceChangeAction :: Update ,
27842807 logical_id : updated. logical_id . clone ( ) ,
@@ -2787,7 +2810,18 @@ pub(crate) fn apply_resource_updates(
27872810 } ) ;
27882811 physical_ids
27892812 . insert ( updated. logical_id . clone ( ) , updated. physical_id . clone ( ) ) ;
2790- attributes. insert ( updated. logical_id . clone ( ) , updated. attributes . clone ( ) ) ;
2813+ // Merge onto the create-time attribute set + well-known
2814+ // GetAtt overlay so a 2nd deploy doesn't collapse Outputs
2815+ // to the update handler's subset (dropping e.g.
2816+ // CreationTime and making GetAtt resolve to literal
2817+ // "Logical.Attr").
2818+ let merged = merged_resource_attributes (
2819+ provisioner,
2820+ & updated,
2821+ existing. attributes . clone ( ) ,
2822+ ) ;
2823+ attributes. insert ( updated. logical_id . clone ( ) , merged. clone ( ) ) ;
2824+ updated. attributes = merged;
27912825 if let Some ( slot) = stack
27922826 . resources
27932827 . iter_mut ( )
0 commit comments