File Coverage

blib/lib/Cfn/Diff.pm
Criterion Covered Total %
statement 88 94 93.6
branch 55 68 80.8
condition 18 30 60.0
subroutine 9 9 100.0
pod 0 1 0.0
total 170 202 84.1


line stmt bran cond sub pod time code
1             package Cfn::Diff::Changes {
2 2     2   2378 use Moose;
  2         4  
  2         17  
3             has path => (is => 'ro', isa => 'Str');
4             has change => (is => 'ro', isa => 'Str');
5             # to and from are left as rw because diff wants to
6             # reassign these properties to the resolved version
7             # when a DynamicValue is found
8             has from => (is => 'rw');
9             has to => (is => 'rw');
10             }
11              
12             package Cfn::Diff::IncompatibleChange {
13 2     2   14289 use Moose;
  2         5  
  2         10  
14             extends 'Cfn::Diff::Changes';
15             }
16              
17             package Cfn::Diff::ResourcePropertyChange {
18 2     2   13211 use Moose;
  2         5  
  2         9  
19             extends 'Cfn::Diff::Changes';
20             has resource => (is => 'ro', isa => 'Cfn::Resource', required => 1);
21             has property => (is => 'ro', isa => 'Str', required => 1);
22              
23             has mutability => (is => 'ro', isa => 'Str|Undef', lazy => 1, default => sub {
24             my $self = shift;
25             my $prop_meta = $self->resource->Properties->meta->find_attribute_by_name($self->property);
26             return undef if (not $prop_meta->does('CfnMutability'));
27             return $prop_meta->mutability;
28             });
29             }
30              
31             package Cfn::Diff {
32 2     2   13393 use Moose;
  2         5  
  2         9  
33              
34             has resolve_dynamicvalues => (
35             is => 'ro',
36             isa => 'Bool',
37             default => 0
38             );
39              
40             sub changes {
41 35     35 0 37639 my $self = shift;
42 35 100       928 return $self->_changes if (defined $self->_changes);
43 18         421 $self->_changes([]);
44 18         58 $self->_do_diff;
45 18         1004 return $self->_changes;
46             }
47              
48             has _changes => (
49             is => 'rw',
50             isa => 'ArrayRef[Cfn::Diff::Changes]',
51             traits => [ 'Array' ],
52             handles => {
53             new_addition => 'push',
54             new_deletion => 'push',
55             new_change => 'push',
56             },
57             );
58              
59             has left => (is => 'ro', isa => 'Cfn', required => 1);
60             has right => (is => 'ro', isa => 'Cfn', required => 1);
61              
62             sub _do_diff {
63 18     18   36 my ($self) = @_;
64 18 100       450 my $old = ($self->resolve_dynamicvalues) ? $self->left->resolve_dynamicvalues : $self->left;
65 18 100       1210 my $new = ($self->resolve_dynamicvalues) ? $self->right->resolve_dynamicvalues : $self->right;
66              
67 18         1302 my %new_resources = map { ( $_ => 1 ) } $new->ResourceList;
  18         76  
68 18         489 my %old_resources = map { ( $_ => 1 ) } $old->ResourceList;
  20         57  
69 18         34 my %changed = ();
70 18         88 foreach my $res (keys %new_resources) {
71 18 100       53 if (exists $old_resources{ $res }) {
72              
73 16 100       456 if (my @changes = $self->_compare_resource($new->Resource($res), $old->Resource($res), $res)) {
74 11         3935 $self->new_change(@changes);
75             }
76              
77 16         35 delete $new_resources{ $res };
78 16         41 delete $old_resources{ $res };
79             } else {
80 2         68 $self->new_addition(Cfn::Diff::Changes->new(path => "Resources.$res", change => 'Resource Added', from => undef, to => $new->Resource($res)));
81 2         8 delete $new_resources{ $res };
82             }
83             }
84 18         65 foreach my $res (keys %old_resources) {
85 4         130 $self->new_deletion(Cfn::Diff::Changes->new(path => "Resources.$res", change => 'Resource Deleted', from => $old->Resource($res), to => undef));
86             }
87             }
88              
89             sub _compare_resource {
90 16     16   35 my ($self, $new_res, $old_res, $logical_id) = @_;
91              
92 16         344 my $new_res_type = $new_res->Type;
93 16 100       115 $new_res_type = 'AWS::CloudFormation::CustomResource' if ($new_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
94 16         325 my $old_res_type = $old_res->Type;
95 16 100       67 $old_res_type = 'AWS::CloudFormation::CustomResource' if ($old_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
96              
97 16 100       43 if ($new_res_type ne $old_res_type) {
98 1         23 return Cfn::Diff::IncompatibleChange->new(
99             path => "Resources.$logical_id",
100             change => 'Resource Type Changed',
101             from => $old_res->Type,
102             to => $new_res->Type,
103             );
104             }
105              
106             # This section diffs the resources properties
107 15         460 my $new = $new_res->Properties;
108 15         423 my $old = $old_res->Properties;
109              
110 15 100 100     108 if (not defined $new and not defined $old) {
    100 100        
111 1         6 return ; # No changes, and don't go on trying to
112             # diff the properties of unexisting objects
113             } elsif (not defined $new or not defined $old) {
114 2         3 my $message;
115 2 100 66     10 $message = "Properties key deleted" if (not defined $new and defined $old);
116 2 100 66     11 $message = "Properties key added" if (defined $new and not defined $old);
117              
118 2         16 return Cfn::Diff::Changes->new(
119             path => "Resources.$logical_id",
120             change => $message,
121             from => $old,
122             to => $new,
123             );
124             }
125              
126             # If we get here, the two objects have properties
127 12         27 my @changes = ();
128 12         52 foreach my $p ($new->meta->get_all_attributes) {
129 259         17702 my $meth = $p->name;
130 259         8271 my $new_val = $new->$meth;
131 259         7779 my $old_val = $old->$meth;
132              
133 259 50 66     893 next if (not defined $new_val and not defined $old_val);
134              
135 22         27 my $change_description;
136 22 50 66     172 if ( defined $old_val and not defined $new_val) {
    100 66        
    50 33        
    0 0        
137 0         0 $change_description = 'Property Deleted';
138             } elsif (not defined $old_val and defined $new_val) {
139 1         2 $change_description = 'Property Added';
140             } elsif ( defined $old_val and defined $new_val) {
141 21 100       76 if (not $self->_properties_equal($new_val, $old_val, "$logical_id.$meth")) {
142 8         56 $change_description = 'Property Changed';
143             } else {
144             next
145 13         36 }
146             } elsif (not defined $old_val and not defined $new_val) {
147 0         0 next;
148             }
149              
150 9         72 push @changes, Cfn::Diff::ResourcePropertyChange->new(
151             path => "Resources.$logical_id.Properties.$meth",
152             change => $change_description,
153             from => $old_val,
154             to => $new_val,
155             resource => $new_res,
156             property => $meth,
157             );
158             }
159 12         1917 return @changes;
160             }
161              
162 2     2   14644 use Scalar::Util;
  2         9  
  2         875  
163             sub _properties_equal {
164 46     46   93 my ($self, $new, $old) = @_;
165              
166 46 100       146 if (blessed($new)){
167 23 50       60 if (blessed($old)){
168             # See if old and new are of the same class
169 23 100       83 return 0 if ($new->meta->name ne $old->meta->name);
170              
171             # Old and new are guaranteed to be the same type now, so just go on with new
172 22 100       929 if ($new->isa('Cfn::DynamicValue')) {
    100          
    100          
    50          
173 1         5 return 0;
174             } elsif ($new->isa('Cfn::Value::Primitive')) {
175 14         350 return ($new->Value eq $old->Value);
176             } elsif ($new->isa('Cfn::Value::Function')) {
177 2   33     53 return (($new->Function eq $old->Function) and $self->_properties_equal($new->Value, $old->Value));
178             } elsif ($new->isa('Cfn::Value')) {
179 5         22 return $self->_properties_equal($new->as_hashref, $old->as_hashref);
180             } else {
181 0         0 die "Don't know how to compare $new";
182             }
183             } else {
184 0         0 return 0;
185             }
186             } else {
187 23 50       51 if (blessed($old)) {
188 0         0 return 0;
189             } else {
190 23 50       56 return 0 if (ref($old) ne ref($new));
191 23 100       56 if (not ref($new)){
    100          
    50          
192 10         44 return ($new eq $old);
193             } elsif (ref($new) eq 'ARRAY') {
194 6 50       14 return 0 if (@$new != @$old);
195 6         15 for (my $i = 0; $i < @$new; $i++) {
196 11 50       62 return 0 if (not $self->_properties_equal($new->[$i], $old->[$i]));
197             }
198 6         23 return 1;
199             } elsif (ref($new) eq 'HASH') {
200 7 50       33 return 0 if ((keys %$new) != (keys %$old));
201 7         17 foreach my $key (keys %$new) {
202 7 100       20 return 0 if (not $self->_properties_equal($new->{ $key }, $old->{ $key }));
203             }
204 5         16 return 1;
205             } else {
206 0           die "Don't know how to non-blessed compare " . ref($new);
207             }
208             }
209             }
210             }
211             }
212              
213             1;