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   2295 use Moose;
  2         6  
  2         15  
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   14000 use Moose;
  2         5  
  2         10  
14             extends 'Cfn::Diff::Changes';
15             }
16              
17             package Cfn::Diff::ResourcePropertyChange {
18 2     2   13192 use Moose;
  2         6  
  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   13089 use Moose;
  2         9  
  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 30675 my $self = shift;
42 35 100       754 return $self->_changes if (defined $self->_changes);
43 18         330 $self->_changes([]);
44 18         47 $self->_do_diff;
45 18         876 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       411 my $old = ($self->resolve_dynamicvalues) ? $self->left->resolve_dynamicvalues : $self->left;
65 18 100       1444 my $new = ($self->resolve_dynamicvalues) ? $self->right->resolve_dynamicvalues : $self->right;
66              
67 18         1379 my %new_resources = map { ( $_ => 1 ) } $new->ResourceList;
  18         57  
68 18         439 my %old_resources = map { ( $_ => 1 ) } $old->ResourceList;
  20         45  
69 18         27 my %changed = ();
70 18         36 foreach my $res (keys %new_resources) {
71 18 100       42 if (exists $old_resources{ $res }) {
72              
73 16 100       372 if (my @changes = $self->_compare_resource($new->Resource($res), $old->Resource($res), $res)) {
74 11         3027 $self->new_change(@changes);
75             }
76              
77 16         28 delete $new_resources{ $res };
78 16         31 delete $old_resources{ $res };
79             } else {
80 2         61 $self->new_addition(Cfn::Diff::Changes->new(path => "Resources.$res", change => 'Resource Added', from => undef, to => $new->Resource($res)));
81 2         10 delete $new_resources{ $res };
82             }
83             }
84 18         58 foreach my $res (keys %old_resources) {
85 4         135 $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   29 my ($self, $new_res, $old_res, $logical_id) = @_;
91              
92 16         276 my $new_res_type = $new_res->Type;
93 16 100       79 $new_res_type = 'AWS::CloudFormation::CustomResource' if ($new_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
94 16         264 my $old_res_type = $old_res->Type;
95 16 100       49 $old_res_type = 'AWS::CloudFormation::CustomResource' if ($old_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
96              
97 16 100       30 if ($new_res_type ne $old_res_type) {
98 1         17 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         385 my $new = $new_res->Properties;
108 15         334 my $old = $old_res->Properties;
109              
110 15 100 100     77 if (not defined $new and not defined $old) {
    100 100        
111 1         4 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         4 my $message;
115 2 100 66     7 $message = "Properties key deleted" if (not defined $new and defined $old);
116 2 100 66     15 $message = "Properties key added" if (defined $new and not defined $old);
117              
118 2         9 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         23 my @changes = ();
128 12         59 foreach my $p ($new->meta->get_all_attributes) {
129 259         16241 my $meth = $p->name;
130 259         6430 my $new_val = $new->$meth;
131 259         5995 my $old_val = $old->$meth;
132              
133 259 50 66     686 next if (not defined $new_val and not defined $old_val);
134              
135 22         30 my $change_description;
136 22 50 66     128 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       64 if (not $self->_properties_equal($new_val, $old_val, "$logical_id.$meth")) {
142 8         40 $change_description = 'Property Changed';
143             } else {
144             next
145 13         25 }
146             } elsif (not defined $old_val and not defined $new_val) {
147 0         0 next;
148             }
149              
150 9         57 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         51 return @changes;
160             }
161              
162 2     2   14468 use Scalar::Util;
  2         4  
  2         867  
163             sub _properties_equal {
164 46     46   75 my ($self, $new, $old) = @_;
165              
166 46 100       95 if (blessed($new)){
167 23 50       45 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       710 if ($new->isa('Cfn::DynamicValue')) {
    100          
    100          
    50          
173 1         5 return 0;
174             } elsif ($new->isa('Cfn::Value::Primitive')) {
175 14         270 return ($new->Value eq $old->Value);
176             } elsif ($new->isa('Cfn::Value::Function')) {
177 2   33     45 return (($new->Function eq $old->Function) and $self->_properties_equal($new->Value, $old->Value));
178             } elsif ($new->isa('Cfn::Value')) {
179 5         13 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       29 if (blessed($old)) {
188 0         0 return 0;
189             } else {
190 23 50       39 return 0 if (ref($old) ne ref($new));
191 23 100       46 if (not ref($new)){
    100          
    50          
192 10         33 return ($new eq $old);
193             } elsif (ref($new) eq 'ARRAY') {
194 6 50       11 return 0 if (@$new != @$old);
195 6         13 for (my $i = 0; $i < @$new; $i++) {
196 11 50       43 return 0 if (not $self->_properties_equal($new->[$i], $old->[$i]));
197             }
198 6         16 return 1;
199             } elsif (ref($new) eq 'HASH') {
200 7 50       22 return 0 if ((keys %$new) != (keys %$old));
201 7         14 foreach my $key (keys %$new) {
202 7 100       39 return 0 if (not $self->_properties_equal($new->{ $key }, $old->{ $key }));
203             }
204 5         14 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;