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   2151 use Moose;
  2         4  
  2         13  
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   12727 use Moose;
  2         5  
  2         9  
14             extends 'Cfn::Diff::Changes';
15             }
16              
17             package Cfn::Diff::ResourcePropertyChange {
18 2     2   11968 use Moose;
  2         4  
  2         8  
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   12246 use Moose;
  2         4  
  2         8  
33              
34             has resolve_dynamicvalues => (
35             is => 'ro',
36             isa => 'Bool',
37             default => 0
38             );
39              
40             sub changes {
41 35     35 0 34046 my $self = shift;
42 35 100       796 return $self->_changes if (defined $self->_changes);
43 18         335 $self->_changes([]);
44 18         60 $self->_do_diff;
45 18         928 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   48 my ($self) = @_;
64 18 100       470 my $old = ($self->resolve_dynamicvalues) ? $self->left->resolve_dynamicvalues : $self->left;
65 18 100       1250 my $new = ($self->resolve_dynamicvalues) ? $self->right->resolve_dynamicvalues : $self->right;
66              
67 18         2272 my %new_resources = map { ( $_ => 1 ) } $new->ResourceList;
  18         61  
68 18         427 my %old_resources = map { ( $_ => 1 ) } $old->ResourceList;
  20         58  
69 18         34 my %changed = ();
70 18         47 foreach my $res (keys %new_resources) {
71 18 100       45 if (exists $old_resources{ $res }) {
72              
73 16 100       402 if (my @changes = $self->_compare_resource($new->Resource($res), $old->Resource($res), $res)) {
74 11         3319 $self->new_change(@changes);
75             }
76              
77 16         33 delete $new_resources{ $res };
78 16         38 delete $old_resources{ $res };
79             } else {
80 2         60 $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         72 foreach my $res (keys %old_resources) {
85 4         124 $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   41 my ($self, $new_res, $old_res, $logical_id) = @_;
91              
92 16         304 my $new_res_type = $new_res->Type;
93 16 100       98 $new_res_type = 'AWS::CloudFormation::CustomResource' if ($new_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
94 16         273 my $old_res_type = $old_res->Type;
95 16 100       72 $old_res_type = 'AWS::CloudFormation::CustomResource' if ($old_res->isa('Cfn::Resource::AWS::CloudFormation::CustomResource'));
96              
97 16 100       48 if ($new_res_type ne $old_res_type) {
98 1         20 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         407 my $new = $new_res->Properties;
108 15         352 my $old = $old_res->Properties;
109              
110 15 100 100     111 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         4 my $message;
115 2 100 66     11 $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         14 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         49 foreach my $p ($new->meta->get_all_attributes) {
129 247         15397 my $meth = $p->name;
130 247         6630 my $new_val = $new->$meth;
131 247         6124 my $old_val = $old->$meth;
132              
133 247 50 66     669 next if (not defined $new_val and not defined $old_val);
134              
135 22         26 my $change_description;
136 22 50 66     140 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         3 $change_description = 'Property Added';
140             } elsif ( defined $old_val and defined $new_val) {
141 21 100       87 if (not $self->_properties_equal($new_val, $old_val, "$logical_id.$meth")) {
142 8         60 $change_description = 'Property Changed';
143             } else {
144             next
145 13         32 }
146             } elsif (not defined $old_val and not defined $new_val) {
147 0         0 next;
148             }
149              
150 9         74 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         1709 return @changes;
160             }
161              
162 2     2   13984 use Scalar::Util;
  2         5  
  2         841  
163             sub _properties_equal {
164 46     46   74 my ($self, $new, $old) = @_;
165              
166 46 100       117 if (blessed($new)){
167 23 50       48 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       773 if ($new->isa('Cfn::DynamicValue')) {
    100          
    100          
    50          
173 1         13 return 0;
174             } elsif ($new->isa('Cfn::Value::Primitive')) {
175 14         288 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         21 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       37 if (blessed($old)) {
188 0         0 return 0;
189             } else {
190 23 50       45 return 0 if (ref($old) ne ref($new));
191 23 100       48 if (not ref($new)){
    100          
    50          
192 10         37 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       46 return 0 if (not $self->_properties_equal($new->[$i], $old->[$i]));
197             }
198 6         28 return 1;
199             } elsif (ref($new) eq 'HASH') {
200 7 50       24 return 0 if ((keys %$new) != (keys %$old));
201 7         16 foreach my $key (keys %$new) {
202 7 100       21 return 0 if (not $self->_properties_equal($new->{ $key }, $old->{ $key }));
203             }
204 5         13 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;