kitchen-scribe 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -0
- data/README.md +89 -19
- data/lib/chef/knife/scribe_adjust.rb +325 -0
- data/lib/chef/knife/scribe_copy.rb +4 -4
- data/lib/chef/knife/scribe_hire.rb +2 -2
- data/lib/kitchen-scribe/version.rb +1 -1
- data/spec/chef/knife/scribe_adjust_spec.rb +916 -0
- data/spec/spec_helper.rb +1 -0
- metadata +5 -2
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -25,6 +25,8 @@ GEM
|
|
25
25
|
highline (1.6.15)
|
26
26
|
ipaddress (0.8.0)
|
27
27
|
json (1.6.1)
|
28
|
+
kitchen-scribe (0.1.0)
|
29
|
+
chef (>= 0.10.10)
|
28
30
|
mime-types (1.19)
|
29
31
|
mixlib-authentication (1.3.0)
|
30
32
|
mixlib-log
|
@@ -70,4 +72,5 @@ PLATFORMS
|
|
70
72
|
|
71
73
|
DEPENDENCIES
|
72
74
|
chef
|
75
|
+
kitchen-scribe
|
73
76
|
rspec
|
data/README.md
CHANGED
@@ -4,33 +4,42 @@ Kitchen Scribe
|
|
4
4
|
DESCRIPTION
|
5
5
|
-----------
|
6
6
|
|
7
|
-
This is a small Knife plugin for keeping track of various changes in your Chef environemnt.
|
7
|
+
This is a small Knife plugin for making and keeping track of various changes in your Chef environemnt.
|
8
8
|
|
9
|
-
|
9
|
+
It performs two main functions.
|
10
10
|
|
11
|
-
|
11
|
+
1. _Documenting changes_. It pulls the configuration of all your environements, roles, and nodes then saves them into json files in the place of your choosing and commits the changes to a local git repository. It can also pull/push them to a remote repostory for safekeeping.
|
12
|
+
|
13
|
+
2. _Making precise changes_. It can perform precise updates on your environments, roles and nodes by using json data structure describing the change. *[This feature is still being tested and it's not available through rubygems yet. If you would like to try it please install it driectly from the github repo]*
|
14
|
+
|
15
|
+
The philosophy behind using scribe to update your environments, roles an nodes is that you may want to make prepare some changes in advance, be able to test them and then have them applied to the final setup. Also it might be important to isolate those changes in a clear way so people who are not familiar with chef don't have to edit a huge json object to get them in. Lastly you can now automate applying your changes as well, and automation is what Chef is all about in the end:).
|
16
|
+
|
17
|
+
The plugin is still in the beta stage, I've tested it manualy to some extent, but I'm sure there are things I missed. Please submit any bugs you find through the github issue system and I promiss to take care of them as soon as possible.
|
12
18
|
|
13
19
|
[![Code Climate](https://codeclimate.com/github/khozlov/kitchen-scribe.png)](https://codeclimate.com/github/khozlov/kitchen-scribe)
|
20
|
+
[![Gem Version](https://badge.fury.io/rb/kitchen-scribe.png)](http://badge.fury.io/rb/kitchen-scribe)
|
14
21
|
|
15
22
|
USAGE
|
16
23
|
-----
|
17
24
|
|
18
|
-
|
25
|
+
Install as gem using `gem install kitchen-scribe`.
|
26
|
+
|
27
|
+
### Documenting Changes
|
19
28
|
|
20
29
|
First you need to hire a scribe using `knife scribe hire`. By default this will initialize a local directory called `.chronicle` where all backups will be performed. This task takes the following parameters:
|
21
30
|
|
22
|
-
*
|
23
|
-
*
|
24
|
-
*
|
31
|
+
* `-p` or `--chronicle-path` followed by a path allows you to specify a different location for the backup dir.
|
32
|
+
* `-r` or `--remote-url` followed by a git remote url will set up a remote in the backup dir pointing to the specified url. By default the name of the remote will be `origin`
|
33
|
+
* `--remote-name` followed by a remote_name allows you to specify a name for the remote repository. If `-r` is not used this has no effect.
|
25
34
|
|
26
35
|
`hire` action can be performed multiple times to set up additional remotes.
|
27
36
|
|
28
|
-
Next you probably want to get your scribe to actually do something for a change. `knife scribe copy` will fully back
|
37
|
+
Next you probably want to get your scribe to actually do something for a change. `knife scribe copy` will fully back your `roles` and `environments` up and partially back your `nodes` up (only `name`, `env`, `attributes` and `run_list`). It assumes that you already hired your scribe and by default will look for your chronicle at `.chronicle`, assume that your remote name is `origin`, your default branch name is `master` and you want each of your commit messages to say _"Commiting chef state as of [current date and time]"_. Again you may customize this using:
|
29
38
|
|
30
|
-
*
|
31
|
-
*
|
32
|
-
*
|
33
|
-
*
|
39
|
+
* `-p` or `--chronicle-path` followed by a path to specify a different location for the chronicle.
|
40
|
+
* `--remote-name` followed by a remote_name to specify a different remote name.
|
41
|
+
* `--branch` followed by a branch name to indicate that you want to use a different branch.
|
42
|
+
* `-m` or `--commit-message` followed by a message to, suprise, suprise, specify your own custom message (use `%TIME%` anywhere in the message to get it substitued with current time).
|
34
43
|
|
35
44
|
You can also specify all the params in your `knife.rb` not to type it in every time by putting a config hash in there:
|
36
45
|
|
@@ -38,15 +47,76 @@ You can also specify all the params in your `knife.rb` not to type it in every t
|
|
38
47
|
:remote_name => "your_remote_name",
|
39
48
|
:remote_url => "your_remote_url",
|
40
49
|
:branch => "your_branch",
|
41
|
-
:commit_message => "your_commit_message"
|
42
|
-
|
43
|
-
|
50
|
+
:commit_message => "your_commit_message"
|
51
|
+
}
|
52
|
+
|
53
|
+
### Making Changes
|
54
|
+
|
55
|
+
`adjust` action is your friend here.
|
56
|
+
|
57
|
+
I takes any amount of filenames (but at least one) with the changes specified in a JSON object. Apart from `author_name`, `author_email` and `description` which aren't mandatory this object needs to contain a property called `adjustments` that will in turn be an array of JSON objects containing at least the following properties:
|
58
|
+
|
59
|
+
* `action` - The actual action to perform.
|
60
|
+
* `type` - What you are trying to update. It can be either `environment`, `role` or `node`
|
61
|
+
* `search` - the search query that will be used to figure out what to update. If a simple string is given (without a `:` character) scribe will assume it's a name and act accordingly
|
62
|
+
* `adjustment` - the hash containing the actual changes
|
63
|
+
|
64
|
+
The action to perform can be one of the following:
|
65
|
+
|
66
|
+
* `merge` - a deep merge that combines the chef object with the adjustment, adding new entries and updating values. Arrays will be combined.
|
67
|
+
* `hash_only_merge` - same as merge but arrays will be overwritten instead of combined (only in Chef version 11.0+).
|
68
|
+
* `overwrite` - a simple merge on the top level of the chef object. Usefull for overwriting run lists.
|
69
|
+
* `delete` - as the name suggests it can be used to delete parts of the config. The adjustment may be an integer or string in which case scribe will attempt to remove this key from the objec at the top level. It can be hash, which scribe treats as a map to the key that needs to be removed. Finally It can be an array which represents a set of changes that needs to be done on a single level. A quick example:
|
70
|
+
|
71
|
+
With a simple envrionment
|
44
72
|
|
45
|
-
|
46
|
-
|
73
|
+
{ "chef_type": "environment"
|
74
|
+
"cookbook_versions": { "apache2": "<= 1.1.8",
|
75
|
+
"apt": "<= 1.4.9"
|
76
|
+
},
|
77
|
+
"default_attributes": { "env" : "dev",
|
78
|
+
"ports" : [ 80, 8080 ],
|
79
|
+
"app" : { "storage_method" : "s3",
|
80
|
+
"storage_url" : "foo.bar"
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
47
84
|
|
48
|
-
|
49
|
-
|
85
|
+
Applying the folowing `delete` adjustment
|
86
|
+
|
87
|
+
{ "default_attributes" : [ "env",
|
88
|
+
{ "app" : "storage_method" },
|
89
|
+
{ "ports" : 0 }
|
90
|
+
],
|
91
|
+
"cookbook_versions" : "apt"
|
92
|
+
}
|
93
|
+
|
94
|
+
Will remove default_attributes/env, default_attributes/app/storage_method, cookbook_versions/apt keys and first port from the default_attributes/ports array. The final product will be:
|
95
|
+
|
96
|
+
{ "chef_type": "environment"
|
97
|
+
"cookbook_versions": { "apache2": "<= 1.1.8" }
|
98
|
+
"default_attributes": { "ports" : [ 8080 ],
|
99
|
+
"app" : { "storage_url" : "foo.bar" }
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
You can use `adjust` with a `-g` or `--generate` option. It will then fill all the files specified with an adjustment template (overwriting any existing content of the files).
|
104
|
+
|
105
|
+
An additional option `-t` or `--type` allows you to decide what adjustment template should be used (possible variants are `environment`, `role` and `node` - `environment` being the default)
|
106
|
+
|
107
|
+
Runing `adjust` with a `--dryrun` option won't update any objects on the server, but will allow you to review the result of your adjustments in a diff format.
|
108
|
+
|
109
|
+
Lastly you can use the `-d` or `--document` option which will surround your adjustments with a `scribe copy` action (initializing the repo with `scribe hire` just in case). It will use the same configuration form your `knife.rb` file that a standalone call to `hire` or `copy` would. You can also use the same command line prameters (`--chronicle-path`, `--remote-name`, `--remote-url` and `--branch`) to set everything up.
|
110
|
+
|
111
|
+
**Important note:** When you're trying to apply a change to all your environemnts, don't use `*:*` as your search term. Chef will then try to apply those changes to the `_default` environemnt which is frozen and can't me modified. Try using `-name:_default` instead.
|
112
|
+
|
113
|
+
Have fun!
|
114
|
+
|
115
|
+
WHAT'S NEXT
|
116
|
+
-----------
|
117
|
+
* Refactoring, refactoring, refactoring
|
118
|
+
* Testing
|
119
|
+
* Bug fixes
|
50
120
|
|
51
121
|
LICENSE
|
52
122
|
-------
|
@@ -0,0 +1,325 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Pawel Kozlowski (<pawel.kozlowski@u2i.com>)
|
3
|
+
# Copyright:: Copyright (c) 2013 Pawel Kozlowski
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'chef/mixin/deep_merge'
|
20
|
+
require 'chef/mixin/shell_out'
|
21
|
+
|
22
|
+
class Chef
|
23
|
+
class Knife
|
24
|
+
class ScribeAdjust < Chef::Knife
|
25
|
+
|
26
|
+
include Chef::Mixin::DeepMerge
|
27
|
+
include Chef::Mixin::ShellOut
|
28
|
+
|
29
|
+
deps do
|
30
|
+
require_relative 'scribe_hire'
|
31
|
+
require_relative 'scribe_copy'
|
32
|
+
end
|
33
|
+
|
34
|
+
TEMPLATE_HASH = { "author_name" => "",
|
35
|
+
"author_email" => "",
|
36
|
+
"description" => "",
|
37
|
+
"adjustments" => []
|
38
|
+
}
|
39
|
+
|
40
|
+
ENVIRONMENT_ADJUSTMENT_TEMPLATE = {
|
41
|
+
"adjustment" => { "default_attributes" => { },
|
42
|
+
"override_attributes" => { },
|
43
|
+
"cookbook_versions" => { }
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
ROLE_ADJUSTMENT_TEMPLATE = {
|
48
|
+
"adjustment" => { "default_attributes" => { },
|
49
|
+
"override_attributes" => { },
|
50
|
+
"run_list" => [ ]
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
NODE_ADJUSTMENT_TEMPLATE = {
|
55
|
+
"adjustment" => { "attributes" => { },
|
56
|
+
"run_list" => [ ]
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
banner "knife scribe adjust FILE [FILE..]"
|
61
|
+
|
62
|
+
option :generate,
|
63
|
+
:short => "-g",
|
64
|
+
:long => "--generate",
|
65
|
+
:description => "generate adjustment templates"
|
66
|
+
|
67
|
+
option :document,
|
68
|
+
:short => "-d",
|
69
|
+
:long => "--document",
|
70
|
+
:description => "document with copy copy"
|
71
|
+
|
72
|
+
option :dryrun,
|
73
|
+
:long => "--dryrun",
|
74
|
+
:description => "do a test run"
|
75
|
+
|
76
|
+
|
77
|
+
option :type,
|
78
|
+
:short => "-t TYPE",
|
79
|
+
:long => "--type TYPE",
|
80
|
+
:description => "generate adjustment templates [environemnt|node|role]",
|
81
|
+
:default => "environment"
|
82
|
+
|
83
|
+
|
84
|
+
option :chronicle_path,
|
85
|
+
:short => "-p PATH",
|
86
|
+
:long => "--chronicle-path PATH",
|
87
|
+
:description => "Path to the directory where the chronicle should be located",
|
88
|
+
:default => nil
|
89
|
+
|
90
|
+
option :remote_name,
|
91
|
+
:long => "--remote-name REMOTE_NAME",
|
92
|
+
:description => "Name of the remote chronicle repository",
|
93
|
+
:default => nil
|
94
|
+
|
95
|
+
option :remote_url,
|
96
|
+
:short => "-r REMOTE_URL",
|
97
|
+
:long => "--remote-url REMOTE_URL",
|
98
|
+
:description => "Url of the remote chronicle repository",
|
99
|
+
:default => nil
|
100
|
+
|
101
|
+
option :branch,
|
102
|
+
:long => "--branch BRANCH_NAME",
|
103
|
+
:description => "Name of the branch you want to use",
|
104
|
+
:default => nil
|
105
|
+
|
106
|
+
|
107
|
+
alias_method :action_merge, :merge
|
108
|
+
alias_method :action_hash_only_merge, :hash_only_merge if respond_to?(:hash_only_merge)
|
109
|
+
|
110
|
+
def changes
|
111
|
+
@changes ||= {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def descriptions
|
115
|
+
@descriptions ||= []
|
116
|
+
end
|
117
|
+
|
118
|
+
def errors
|
119
|
+
@errors ||= []
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def run
|
124
|
+
if @name_args[0].nil?
|
125
|
+
show_usage
|
126
|
+
ui.fatal("At least one adjustment file needs to be specified!")
|
127
|
+
exit 1
|
128
|
+
end
|
129
|
+
if config[:generate] == true
|
130
|
+
@name_args.each { |filename| generate_template(filename) }
|
131
|
+
else
|
132
|
+
parse_adjustments
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def generate_templates
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_adjustments
|
141
|
+
@name_args.each do |filename|
|
142
|
+
errors.push({ "name" => filename, "general" => nil, "adjustments" => {} })
|
143
|
+
parse_adjustment_file(filename)
|
144
|
+
end
|
145
|
+
if errors?
|
146
|
+
print_errors
|
147
|
+
exit 1 unless config[:dryrun]
|
148
|
+
end
|
149
|
+
if config[:dryrun]
|
150
|
+
diff
|
151
|
+
else
|
152
|
+
if config[:document] == true
|
153
|
+
hire
|
154
|
+
record_state
|
155
|
+
end
|
156
|
+
write_adjustments
|
157
|
+
record_state(descriptions.join("\n").strip) if config[:document] == true
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def generate_template(filename)
|
162
|
+
unless ["environment", "role", "node"].include?(config[:type])
|
163
|
+
ui.fatal("Incorrect adjustment type! Only 'node', 'environment' or 'role' allowed.")
|
164
|
+
exit 1
|
165
|
+
end
|
166
|
+
TEMPLATE_HASH["adjustments"] = [self.class.class_eval(config[:type].upcase + "_ADJUSTMENT_TEMPLATE")]
|
167
|
+
File.open(filename, "w") { |file| file.write(JSON.pretty_generate(TEMPLATE_HASH)) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def parse_adjustment_file(filename)
|
171
|
+
if !File.exists?(filename)
|
172
|
+
errors.last["general"] = "File does not exist!"
|
173
|
+
else
|
174
|
+
begin
|
175
|
+
adjustment_file = File.open(filename, "r") { |file| JSON.load(file) }
|
176
|
+
if adjustment_file_valid? adjustment_file
|
177
|
+
adjustment_file["adjustments"].each_with_index do |adjustment, index|
|
178
|
+
apply_adjustment(adjustment) if adjustment_valid?(adjustment, index)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
if adjustment_file["adjustments"].length > errors.last["adjustments"].length
|
182
|
+
description = adjustment_file["description"]
|
183
|
+
description += "[with errors]" if errors.last["adjustments"].size > 0
|
184
|
+
descriptions.push(description)
|
185
|
+
end
|
186
|
+
rescue JSON::ParserError
|
187
|
+
errors.last["general"] = "Malformed JSON!"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def apply_adjustment(adjustment)
|
193
|
+
query = adjustment["search"].include?(":") ? adjustment["search"] : "name:" + adjustment["search"]
|
194
|
+
Chef::Search::Query.new.search(adjustment["type"], query ) do |result|
|
195
|
+
result_hash = result.to_hash
|
196
|
+
key = result_hash["chef_type"] + ":" + result_hash["name"]
|
197
|
+
if changes.has_key? key
|
198
|
+
result_hash = changes[key]["adjusted"]
|
199
|
+
else
|
200
|
+
changes.store(key, { "original" => result_hash })
|
201
|
+
end
|
202
|
+
changes[key].store("adjusted", send(("action_" + adjustment["action"]).to_sym, result_hash, adjustment["adjustment"]))
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def write_adjustments
|
207
|
+
changes.values.each do |change|
|
208
|
+
Chef.const_get(change["adjusted"]["chef_type"].capitalize).json_create(change["adjusted"]).save
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def adjustment_file_valid? adjustment_file
|
213
|
+
unless adjustment_file.kind_of?(Hash)
|
214
|
+
errors.last["general"] = "Adjustment file must contain a JSON hash!"
|
215
|
+
return false
|
216
|
+
end
|
217
|
+
|
218
|
+
unless adjustment_file["adjustments"].kind_of?(Array)
|
219
|
+
errors.last["general"] = "Adjustment file must contain an array of adjustments!"
|
220
|
+
return false
|
221
|
+
end
|
222
|
+
true
|
223
|
+
end
|
224
|
+
|
225
|
+
def adjustment_valid?(adjustment, index)
|
226
|
+
unless adjustment.kind_of?(Hash)
|
227
|
+
errors.last["adjustments"].store(index,"Adjustment must be a JSON hash!")
|
228
|
+
return false
|
229
|
+
end
|
230
|
+
|
231
|
+
["action", "type", "search", "adjustment"].each do |required_key|
|
232
|
+
unless adjustment.has_key?(required_key)
|
233
|
+
errors.last["adjustments"].store(index, "Adjustment hash must contain " + required_key + "!")
|
234
|
+
return false
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
unless respond_to?("action_" + adjustment["action"])
|
239
|
+
errors.last["adjustments"].store(index, "Incorrect action!")
|
240
|
+
return false
|
241
|
+
end
|
242
|
+
true
|
243
|
+
end
|
244
|
+
|
245
|
+
def hire
|
246
|
+
hired_scribe = Chef::Knife::ScribeHire.new
|
247
|
+
[:chronicle_path, :remote_url, :remote_name].each { |key| hired_scribe.config[key] = config[key] }
|
248
|
+
hired_scribe.run
|
249
|
+
end
|
250
|
+
|
251
|
+
def record_state(message = nil)
|
252
|
+
if @copyist.nil?
|
253
|
+
@copyist = Chef::Knife::ScribeCopy.new
|
254
|
+
[:chronicle_path, :remote_name, :branch].each { |key| @copyist.config[key] = config[key] }
|
255
|
+
end
|
256
|
+
@copyist.config[:message] = message
|
257
|
+
@copyist.run
|
258
|
+
end
|
259
|
+
|
260
|
+
def errors?
|
261
|
+
errors.each { |err| return true if !err["general"].nil? || (err["adjustments"].size > 0) }
|
262
|
+
false
|
263
|
+
end
|
264
|
+
|
265
|
+
def print_errors
|
266
|
+
ui.error("ERRORS OCCURED:")
|
267
|
+
errors.each do |err|
|
268
|
+
ui.error(err["name"]) if !err["general"].nil? || (err["adjustments"].size > 0)
|
269
|
+
ui.error("\t" + err["general"]) if !err["general"].nil?
|
270
|
+
err["adjustments"].each { |num, adj_err| ui.error("\t[Adjustment #{num}]: #{adj_err}") }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def action_overwrite(base, overwrite_with)
|
275
|
+
if base.kind_of?(Hash) && overwrite_with.kind_of?(Hash)
|
276
|
+
base.merge(overwrite_with)
|
277
|
+
elsif overwrite_with.nil?
|
278
|
+
base
|
279
|
+
else
|
280
|
+
overwrite_with
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def deep_delete(delete_from, delete_spec)
|
286
|
+
deep_delete!(delete_from.dup, delete_spec.dup)
|
287
|
+
end
|
288
|
+
|
289
|
+
alias_method :action_delete, :deep_delete
|
290
|
+
|
291
|
+
def deep_delete!(delete_from, delete_spec)
|
292
|
+
if delete_from.kind_of?(Hash) || delete_from.kind_of?(Array)
|
293
|
+
if delete_spec.kind_of?(Array)
|
294
|
+
delete_spec.each { |item| deep_delete!(delete_from, item) }
|
295
|
+
elsif delete_spec.kind_of?(Hash)
|
296
|
+
delete_spec.each { |key,item| deep_delete!(delete_from[key], item) }
|
297
|
+
else
|
298
|
+
delete_from.kind_of?(Array) ? delete_from.delete_at(delete_spec) : delete_from.delete(delete_spec)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
delete_from
|
302
|
+
end
|
303
|
+
|
304
|
+
def diff
|
305
|
+
original_file = Tempfile.new("original")
|
306
|
+
adjusted_file = Tempfile.new("adjusted")
|
307
|
+
begin
|
308
|
+
changes.each do |key, change|
|
309
|
+
ui.info("[#{key}]")
|
310
|
+
original_file.write(JSON.pretty_generate(change["original"]))
|
311
|
+
adjusted_file.write(JSON.pretty_generate(change["adjusted"]))
|
312
|
+
original_file.rewind
|
313
|
+
adjusted_file.rewind
|
314
|
+
diff_output = shell_out("diff -L original -L adjusted -u #{original_file.path} #{adjusted_file.path}")
|
315
|
+
ui.info(diff_output.stdout)
|
316
|
+
end
|
317
|
+
ensure
|
318
|
+
original_file.close
|
319
|
+
original_file.unlink
|
320
|
+
adjusted_file.close
|
321
|
+
adjusted_file.unlink
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|