chef_synchronize 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72b4321e9cf345928e25d635dac34f33686cf6c6
4
+ data.tar.gz: 4ecd34bd2ff2b46c35a39e20df415cd680d5f25d
5
+ SHA512:
6
+ metadata.gz: 5ac32717abb2b45d076748c766b7c2e6a3bfbf43aefe273576841583380f7b88c2e377867e04db663f5a1688e03ac4fd14ea87c170866dae2e08acaf2d42b36b
7
+ data.tar.gz: 906c3f0d0b5a0be5fa2d9654023b90dbdd249e054cccc2d2f8d7830bbbfe48189cf79753a84c08b8ee01fde0b1d8df46e7df86b04228ce810c69ade786c6fe78
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Cozy Services Ltd.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,66 @@
1
+ # ChefSync
2
+
3
+ Sync a monolithic chef repo to a chef server.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your chef repo's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'chef_synchronize'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install chef_synchronize
20
+
21
+ ## Configuration
22
+
23
+ `chef_synchronize` requires configuration to post to Slack (only required if you want
24
+ to post to Slack) and to communicate with the Chef server via Knife and Ridley.
25
+
26
+ To configure Slack, you must set the `CHEFSYNC_SLACK_WEBHOOK_URL` environment
27
+ variable. You can optionally also set `CHEFSYNC_SLACK_USERNAME` to set the
28
+ username you'd like to post to Slack under, and `CHEFSYNC_SLACK_CHANNEL` to set
29
+ the Slack channel.
30
+
31
+ You can also optionally set `CHEFSYNC_CI_BUILD_URL` and `CHEFSYNC_COMMIT_URL`
32
+ environment variables. If you set both, they will appear as links in the Slack
33
+ post's pretext above the results of the sync.
34
+
35
+ To configure Knife/Ridley, you must have a `.chef` directory in your PATH that
36
+ contains a `.knife.rb` config file.
37
+
38
+ ## Usage
39
+
40
+ From within your chef repo, execute the following line to see a list of
41
+ unsynced changes:
42
+
43
+ $ bundle exec chef_sync
44
+
45
+ By default, `chef_sync` is set to dryrun mode, where `chef_sync` will tell you
46
+ what updates would happen without actually syncing things to the Chef server,
47
+ and to avoid posting to Slack. The output will be printed to the console
48
+ regardless of whether it's set to post to Slack.
49
+
50
+ $ bundle exec chef_sync --help
51
+ # help menu
52
+ $ bundle exec chef_sync --no-dryrun
53
+ # runs chef_sync and actually syncs changes to Chef server
54
+ $ bundle exec chef_sync -p
55
+ # runs chef_sync and posts output to Slack in addition to in the console
56
+
57
+ ## Contributing
58
+
59
+ 1. Fork it ( https://github.com/[my-github-username]/chef_synchronize/fork )
60
+ 2. Install dependencies (`bundle install`)
61
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 4. Make your changes.
63
+ 5. Run the tests and make sure they pass (`bundle exec rspec`)
64
+ 6. Commit your changes (`git commit -am 'Add some feature'`)
65
+ 7. Push to the branch (`git push origin my-new-feature`)
66
+ 8. Create a new pull request.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'optparse'
6
+ require 'chef_sync'
7
+
8
+ options = {
9
+ dryrun: true,
10
+ post_to_slack: false
11
+ }
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: chef-sync [options]'
15
+
16
+ opts.on('-d', '--[no-]dryrun',
17
+ 'Print out changes without actually syncing to chef server (default: true)') do |d|
18
+ options[:dryrun] = d
19
+ end
20
+
21
+ opts.on('-p', '--post-to-slack',
22
+ 'Post a summary of changes to Slack (default: false)') do |p|
23
+ options[:post_to_slack] = p
24
+ end
25
+ end.parse!
26
+
27
+ puts ChefSync.new(options[:post_to_slack], options[:dryrun]).run
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+ require 'slack/post'
3
+
4
+ class ChefSync
5
+
6
+ require 'chef_sync/chef_component'
7
+ require 'chef_sync/chef_component/cookbook'
8
+ require 'chef_sync/chef_component/data_bag_item'
9
+ require 'chef_sync/chef_component/environment'
10
+ require 'chef_sync/chef_component/role'
11
+ require 'chef_sync/knife'
12
+
13
+ RESOURCE_TYPES = [Role, Environment, DataBagItem, Cookbook]
14
+
15
+ DRYRUN_MESSAGE = "This was a dry run. Nothing has been updated on the chef server. "
16
+ DEFAULT_LOG_MESSAGE = "There were no changes."
17
+
18
+ def initialize(slack=false,dryrun=true)
19
+ @slack = slack
20
+ @dryrun = dryrun
21
+ @summary = ""
22
+ @log = []
23
+ end
24
+
25
+ def run
26
+ @summary = DRYRUN_MESSAGE.dup if @dryrun
27
+
28
+ RESOURCE_TYPES.each do |resource|
29
+ responses = resource.changes(@dryrun)
30
+ @summary << "#{responses.count}/#{resource.total_resources} #{resource.resource_type}s have changed. "
31
+ @log += responses
32
+ end
33
+
34
+ @log << DEFAULT_LOG_MESSAGE if @log.empty?
35
+
36
+ self.post_to_slack if @slack
37
+ return @summary, @log
38
+ end
39
+
40
+ def post_to_slack
41
+ opts = { webhook_url: ENV['CHEFSYNC_SLACK_WEBHOOK_URL'] }
42
+ opts[:username] = ENV['CHEFSYNC_SLACK_USERNAME'] if ENV['CHEFSYNC_SLACK_USERNAME']
43
+ opts[:channel] = ENV['CHEFSYNC_SLACK_CHANNEL'] if ENV['CHEFSYNC_SLACK_CHANNEL']
44
+
45
+ ::Slack::Post.configure( opts )
46
+ begin
47
+ ::Slack::Post.post_with_attachments(self.pretext, self.slack_attachment)
48
+ #Assuming that a RuntimeError is due to improperly configured Slack::Post.
49
+ rescue RuntimeError => e
50
+ puts "Couldn't post to Slack: #{e}"
51
+ end
52
+ end
53
+
54
+ def slack_attachment
55
+ [
56
+ {
57
+ fallback: @summary,
58
+ fields: [
59
+ {
60
+ title: 'Summary',
61
+ value: @summary,
62
+ short: false
63
+ },
64
+ {
65
+ title: 'Changes',
66
+ value: @log.join("\n"),
67
+ short: false
68
+ }
69
+ ]
70
+ }
71
+ ]
72
+ end
73
+
74
+ def pretext
75
+ if ENV['CHEFSYNC_CI_BUILD_URL'].nil? or ENV['CHEFSYNC_COMMIT_URL'].nil?
76
+ return "chef-sync run triggered."
77
+ else
78
+ return "<#{ENV['CHEFSYNC_CI_BUILD_URL']}|CI build> triggered by <#{ENV['CHEFSYNC_COMMIT_URL']}|commit>."
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,117 @@
1
+ require 'tqdm'
2
+
3
+ class ChefSync
4
+ class ChefComponent
5
+
6
+ CHANGE_LOG_SUMMARIES = {
7
+ :create => " was created.",
8
+ :update => " was updated."
9
+ }
10
+
11
+ ACTIONABLE_CHANGES = [:create, :update]
12
+
13
+ FILE_EXTENSION = ".json"
14
+
15
+ class << self; attr_reader :resource_type end
16
+ class << self; attr_accessor :total_resources end
17
+
18
+ attr_reader :name
19
+ attr_reader :change
20
+
21
+ def initialize(name:, local_knife:, remote_knife:, dryrun:)
22
+ @name = name
23
+ @name_with_extension = name + FILE_EXTENSION
24
+
25
+ @local_knife = local_knife
26
+ @remote_knife = remote_knife
27
+ @dryrun = dryrun
28
+
29
+ @change = :none
30
+ end
31
+
32
+ def self.each(dryrun)
33
+ return enum_for(:each, dryrun) unless block_given?
34
+
35
+ local_knife = self.make_local_knife
36
+ remote_knife = self.make_remote_knife
37
+
38
+ local_resources = self.get_local_resources(local_knife, remote_knife)
39
+ self.total_resources = local_resources.count
40
+
41
+ default_args = {local_knife: local_knife, remote_knife: remote_knife, dryrun: dryrun}
42
+ local_resources.tqdm(leave: true, desc: "Checking #{self.resource_type}s").each do |args|
43
+ resource = self.new(args.merge(default_args))
44
+ resource.sync
45
+ yield resource
46
+ end
47
+ end
48
+
49
+ def self.changes(dryrun)
50
+ return self.each(dryrun).select(&:changed?).flat_map(&:summarize_changes)
51
+ end
52
+
53
+ def self.get_local_resources(local_knife, remote_knife)
54
+ local_resources = local_knife.list
55
+ return local_resources.map {|resource_name| {name: resource_name}}
56
+ end
57
+
58
+ def self.make_local_knife
59
+ return ChefSync::Knife.new(self.resource_type, :local)
60
+ end
61
+
62
+ def self.make_remote_knife
63
+ return ChefSync::Knife.new(self.resource_type, :remote)
64
+ end
65
+
66
+ def changed?
67
+ return @change != :none
68
+ end
69
+
70
+ def actionable_change?
71
+ return ACTIONABLE_CHANGES.include?(@change)
72
+ end
73
+
74
+ def summarize_changes
75
+ return self.resource_path + CHANGE_LOG_SUMMARIES[@change]
76
+ end
77
+
78
+ def resource_path
79
+ return "#{self.class.resource_type}s/#{@name}"
80
+ end
81
+
82
+ def get_local_resource
83
+ return @local_knife.show(@name)
84
+ end
85
+
86
+ def get_remote_resource
87
+ return @remote_knife.show(@name)
88
+ end
89
+
90
+ def upload_resource
91
+ return @remote_knife.upload(@name_with_extension)
92
+ end
93
+
94
+ def compare_local_and_remote_versions
95
+ local_resource = self.get_local_resource
96
+ remote_resource = self.get_remote_resource
97
+
98
+ case
99
+ when remote_resource.empty?
100
+ @change = :create
101
+ when local_resource != remote_resource
102
+ @change = :update
103
+ end
104
+
105
+ return @change
106
+ end
107
+
108
+ def sync
109
+ action = self.compare_local_and_remote_versions
110
+ if !@dryrun and self.actionable_change?
111
+ self.upload_resource
112
+ end
113
+ return action
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,99 @@
1
+ require 'chef'
2
+ require 'ridley'
3
+ require 'mixlib/versioning'
4
+
5
+ Ridley::Logging.logger.level = Logger.const_get('ERROR')
6
+
7
+ class ChefSync::Cookbook < ChefSync::ChefComponent
8
+
9
+ CHANGE_LOG_SUMMARIES = {
10
+ :create => " was created.",
11
+ :update => " was updated.",
12
+ :version_regressed => " is newer than the local version.",
13
+ :version_changed => " has changed without a version number increase."
14
+ }
15
+
16
+ FILE_CHANGE_LOG_SUMMARIES = {
17
+ :file_changed => " has changed.",
18
+ :file_missing => " does not exist locally."
19
+ }
20
+
21
+ @resource_type = 'cookbook'
22
+
23
+ attr_reader :file_change_log
24
+
25
+ def initialize(local_version_number:, remote_version_number:, **opts)
26
+ @local_version_number = Mixlib::Versioning.parse(local_version_number)
27
+ @remote_version_number = Mixlib::Versioning.parse(remote_version_number)
28
+ @file_change_log = {}
29
+
30
+ super(opts)
31
+ end
32
+
33
+ def self.get_local_resources(local_knife, remote_knife)
34
+ local_cookbooks = local_knife.list
35
+ remote_cookbooks = remote_knife.list
36
+
37
+ return local_cookbooks.map do |cb, local_ver|
38
+ {name: cb, local_version_number: local_ver, remote_version_number: remote_cookbooks[cb]}
39
+ end
40
+ end
41
+
42
+ def version_changed?
43
+ return @change == :version_changed
44
+ end
45
+
46
+ def summarize_changes
47
+ self.actionable_change? ? prefix = "" : prefix = "WARNING: "
48
+ summary = [prefix + self.resource_path + CHANGE_LOG_SUMMARIES[@change]]
49
+ if self.version_changed?
50
+ summary << @file_change_log.map {|file, file_action| file + FILE_CHANGE_LOG_SUMMARIES[file_action]}
51
+ end
52
+ return summary
53
+ end
54
+
55
+ def get_remote_resource
56
+ ridley = Ridley.from_chef_config
57
+ remote_cookbook = ridley.cookbook.find(@name, @remote_version_number)
58
+ remote_cookbook_files = Chef::CookbookVersion::COOKBOOK_SEGMENTS.collect { |d| remote_cookbook.method(d).call }.flatten
59
+ return remote_cookbook_files
60
+ end
61
+
62
+ def upload_resource
63
+ return @remote_knife.upload(@name, '--freeze')
64
+ end
65
+
66
+ def compare_cookbook_files
67
+ remote_cookbook_files = self.get_remote_resource
68
+
69
+ remote_cookbook_files.each do |remote_file|
70
+ local_file_path = "#{self.resource_path}/#{remote_file['path']}"
71
+ begin
72
+ local_file_checksum = Chef::CookbookVersion.checksum_cookbook_file(File.open(local_file_path))
73
+ @file_change_log[local_file_path] = :file_changed unless local_file_checksum == remote_file['checksum']
74
+ rescue Errno::ENOENT => e
75
+ @file_change_log[local_file_path] = :file_missing
76
+ end
77
+ end
78
+ return @file_change_log
79
+ end
80
+
81
+ def compare_local_and_remote_versions
82
+ local_ver = @local_version_number
83
+ remote_ver = @remote_version_number
84
+
85
+ case
86
+ when !@remote_version_number
87
+ @change = :create
88
+ when @local_version_number < @remote_version_number
89
+ @change = :version_regressed
90
+ when @local_version_number == @remote_version_number
91
+ @change = :version_changed unless self.compare_cookbook_files.empty?
92
+ when @local_version_number > @remote_version_number
93
+ @change = :update
94
+ end
95
+
96
+ return @change
97
+ end
98
+
99
+ end
@@ -0,0 +1,35 @@
1
+ class ChefSync::DataBagItem < ChefSync::ChefComponent
2
+
3
+ @resource_type = 'data_bag'
4
+
5
+ def initialize(data_bag:, **opts)
6
+ @data_bag = data_bag
7
+
8
+ super(opts)
9
+ end
10
+
11
+ def self.get_local_resources(local_knife, remote_knife)
12
+ local_data_bag_list = local_knife.list
13
+
14
+ return local_data_bag_list.flat_map do |dbag|
15
+ local_knife.show(dbag).map {|item| {name: item, data_bag: dbag}}
16
+ end
17
+ end
18
+
19
+ def resource_path
20
+ return "#{self.class.resource_type}s/#{@data_bag}/#{@name}"
21
+ end
22
+
23
+ def get_local_resource
24
+ return @local_knife.show(@data_bag, @name)
25
+ end
26
+
27
+ def get_remote_resource
28
+ return @remote_knife.show(@data_bag, @name)
29
+ end
30
+
31
+ def upload_resource
32
+ return @remote_knife.upload(@data_bag, @name_with_extension)
33
+ end
34
+
35
+ end
@@ -0,0 +1,5 @@
1
+ class ChefSync::Environment < ChefSync::ChefComponent
2
+
3
+ @resource_type = 'environment'
4
+
5
+ end
@@ -0,0 +1,5 @@
1
+ class ChefSync::Role < ChefSync::ChefComponent
2
+
3
+ @resource_type = 'role'
4
+
5
+ end
@@ -0,0 +1,113 @@
1
+ require 'json'
2
+ require 'knife/api'
3
+
4
+ class ChefSync
5
+ class Knife
6
+
7
+ #Need to extend Chef::Knife::API in this class because knife_capture is top-level.
8
+ extend Chef::Knife::API
9
+
10
+ attr_reader :chef_component
11
+ attr_reader :list_command
12
+ attr_reader :show_command
13
+ attr_reader :upload_command
14
+
15
+ def initialize(chef_component, mode)
16
+ @chef_component = chef_component
17
+ @list_command = "#{chef_component}_list".to_sym
18
+ @show_command = "#{chef_component}_show".to_sym
19
+
20
+ if chef_component == 'cookbook'
21
+ @upload_command = "#{chef_component}_upload".to_sym
22
+ else
23
+ @upload_command = "#{chef_component}_from_file".to_sym
24
+ end
25
+
26
+ @mode = mode
27
+ end
28
+
29
+ # This is the hackiest hack ever.
30
+ # Chef::Knife keeps a persistent option state -- if you add
31
+ # an option like `--local-mode` once, it will be used on
32
+ # _every_ future call to Chef::Knife. I would _love_ to
33
+ # figure out how to change this behavior (lost many hours trying),
34
+ # but until then, we fork a new process for `knife_capture` so
35
+ # that we do not taint our global state.
36
+ # This has to stay a class method for the same reason.
37
+ def self.fork_knife_capture(command, args)
38
+ reader, writer = IO.pipe
39
+
40
+ pid = fork do
41
+ reader.close
42
+ output, stderr, status = knife_capture(command, args)
43
+ Marshal.dump([output, stderr, status], writer)
44
+ end
45
+
46
+ writer.close
47
+ command_output = reader.read
48
+ Process.wait(pid)
49
+ return Marshal.load(command_output)
50
+ end
51
+
52
+ # This instance method exists so we can mock out Knife's output for
53
+ # individual instances when testing.
54
+ def fork_knife_capture(command, args)
55
+ return self.class.fork_knife_capture(command, args)
56
+ end
57
+
58
+ def local?
59
+ return @mode == :local
60
+ end
61
+
62
+ def parse_output(command, args, output)
63
+ stdout, stderr, status = output
64
+
65
+ begin
66
+ return JSON.parse(stdout)
67
+ #Assuming here that a parser error means no data was returned and there was a knife error.
68
+ rescue JSON::ParserError => e
69
+ puts "Received STDERR #{stderr} when trying to run knife_capture(#{command}, #{args})."
70
+ return stdout
71
+ end
72
+ end
73
+
74
+ #Helper method to parse knife cookbook list into cookbooks.
75
+ def format_cookbook_list_output(knife_output)
76
+ cookbooks = {}
77
+ knife_output.each do |c|
78
+ cb, ver = c.gsub(/\s+/m, ' ').strip.split(" ")
79
+ cookbooks[cb] = ver
80
+ end
81
+ return cookbooks
82
+ end
83
+
84
+ def capture_output(command, args)
85
+ args << '-fj'
86
+ args << '-z' if self.local?
87
+ knife_output = self.fork_knife_capture(command, args)
88
+ return self.parse_output(command, args, knife_output)
89
+ end
90
+
91
+ def list(*args)
92
+ parsed_output = self.capture_output(self.list_command, args)
93
+ if self.chef_component == 'cookbook'
94
+ parsed_output = self.format_cookbook_list_output(parsed_output)
95
+ end
96
+ return parsed_output
97
+ end
98
+
99
+ def show(*args)
100
+ return self.capture_output(self.show_command, args)
101
+ end
102
+
103
+ def upload(*args)
104
+ knife_output = self.fork_knife_capture(self.upload_command, args)
105
+ stdout, stderr, status = knife_output
106
+ unless status == 0
107
+ puts "Received STDERR #{stderr} when trying to run knife_capture(#{self.upload_command}, #{args})."
108
+ return false
109
+ end
110
+ return true
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ class ChefSync
2
+ VERSION = "1.0.7"
3
+ end
@@ -0,0 +1,117 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe 'ChefSync::Cookbook' do
4
+
5
+ let(:local_knife) do
6
+ ChefSync::KnifeMock.new(ChefSync::Cookbook.resource_type, :local)
7
+ end
8
+
9
+ let(:remote_knife) do
10
+ ChefSync::KnifeMock.new(ChefSync::Cookbook.resource_type, :remote)
11
+ end
12
+
13
+ let(:dryrun_args) do
14
+ {
15
+ name: 'boyardee',
16
+ local_knife: local_knife,
17
+ remote_knife: remote_knife,
18
+ dryrun: true
19
+ }
20
+ end
21
+
22
+ let(:no_dryrun_args) {dryrun_args.merge(dryrun: false)}
23
+
24
+ context 'when the local and remote are the same version number' do
25
+
26
+ let(:args) do
27
+ dryrun_args.merge({local_version_number: '0.1.0', remote_version_number: '0.1.0'})
28
+ end
29
+
30
+ it 'has no required actions when all files are the same' do
31
+ cb = ChefSync::Cookbook.new(args)
32
+ expect(cb).to receive(:upload_resource).never
33
+ expect(cb).to receive(:compare_cookbook_files).and_return([])
34
+
35
+ action = cb.sync
36
+ expect(action).to be_a(Symbol)
37
+ expect(action).to eq(:none)
38
+ end
39
+
40
+ it 'needs to be updated when a file is different' do
41
+ cb = ChefSync::Cookbook.new(args)
42
+ expect(cb).to receive(:upload_resource).never
43
+ expect(cb).to receive(:compare_cookbook_files).and_return([{'spaghetti' => :file_changed}])
44
+
45
+ action = cb.sync
46
+ expect(action).to be_a(Symbol)
47
+ expect(action).to eq(:version_changed)
48
+ end
49
+
50
+ it 'needs to be updated when a file does not exist locally' do
51
+ cb = ChefSync::Cookbook.new(args)
52
+ expect(cb).to receive(:upload_resource).never
53
+ expect(cb).to receive(:compare_cookbook_files).and_return([{'meatballs' => :file_missing}])
54
+
55
+ action = cb.sync
56
+ expect(action).to be_a(Symbol)
57
+ expect(action).to eq(:version_changed)
58
+ end
59
+ end
60
+
61
+ context 'when the local version is newer' do
62
+
63
+ let(:version_args) { {local_version_number: '0.1.10', remote_version_number: '0.1.9'} }
64
+
65
+ it 'needs to be updated' do
66
+ cb = ChefSync::Cookbook.new(dryrun_args.merge(version_args))
67
+ expect(cb).to receive(:upload_resource).never
68
+
69
+ action = cb.sync
70
+ expect(action).to be_a(Symbol)
71
+ expect(action).to eq(:update)
72
+ end
73
+
74
+ it 'uploads the new cookbook version when not in dryrun mode' do
75
+ remote_knife.set_success("")
76
+
77
+ cb = ChefSync::Cookbook.new(no_dryrun_args.merge(version_args))
78
+ expect(cb).to receive(:upload_resource)
79
+ cb.sync
80
+ end
81
+ end
82
+
83
+ context 'when the local version is older' do
84
+ it 'returns an error message' do
85
+ version_args = {local_version_number: '0.1.9', remote_version_number: '0.1.10'}
86
+ cb = ChefSync::Cookbook.new(dryrun_args.merge(version_args))
87
+ expect(cb).to receive(:upload_resource).never
88
+
89
+ action = cb.sync
90
+ expect(action).to be_a(Symbol)
91
+ expect(action).to eq(:version_regressed)
92
+ end
93
+ end
94
+
95
+ context 'when the remote does not exist' do
96
+
97
+ let(:version_args) { {local_version_number: '0.1.0', remote_version_number: nil} }
98
+
99
+ it 'needs to be created' do
100
+ cb = ChefSync::Cookbook.new(dryrun_args.merge(version_args))
101
+ expect(cb).to receive(:upload_resource).never
102
+
103
+ action = cb.sync
104
+ expect(action).to be_a(Symbol)
105
+ expect(action).to eq(:create)
106
+ end
107
+
108
+ it 'uploads the new cookbook when not in dryrun mode' do
109
+ remote_knife.set_success("")
110
+
111
+ cb = ChefSync::Cookbook.new(no_dryrun_args.merge(version_args))
112
+ expect(cb).to receive(:upload_resource)
113
+ cb.sync
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../../spec_helper'
2
+ require_relative '../chef_component_shared_behaviors'
3
+
4
+ describe 'ChefSync::DataBagItem' do
5
+
6
+ let(:resource_class) {ChefSync::DataBagItem}
7
+
8
+ let(:local_resource) do
9
+ {
10
+ 'id': 'fake_dbag',
11
+ 'data': {
12
+ 'stuff': 'fake data'
13
+ }
14
+ }
15
+ end
16
+
17
+ let(:remote_resource) do
18
+ local_resource.merge({'data' => {'stuff' => 'different fake data'}})
19
+ end
20
+
21
+ let(:init_args) { {name: 'fake_data_bag_item', data_bag: 'fake_data_bag'} }
22
+
23
+ it_should_behave_like 'a chef resource'
24
+
25
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../../spec_helper'
2
+ require_relative '../chef_component_shared_behaviors'
3
+
4
+ describe 'ChefSync::Environment' do
5
+
6
+ let(:resource_class) {ChefSync::Environment}
7
+
8
+ let(:local_resource) do
9
+ {
10
+ 'name': 'fake_environment',
11
+ 'default_attributes': {},
12
+ 'override_attributes': {},
13
+ 'json_class': 'Chef::Environment',
14
+ 'description': 'Fake chef environment.',
15
+ 'cookbook_versions': {},
16
+ 'chef_type': 'environment'
17
+ }
18
+ end
19
+
20
+ let(:remote_resource) do
21
+ local_resource.merge({'description' => 'This is a different fake environment.'})
22
+ end
23
+
24
+ let(:init_args) { {name: 'fake_environment'} }
25
+
26
+ it_should_behave_like 'a chef resource'
27
+
28
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../../spec_helper'
2
+ require_relative '../chef_component_shared_behaviors'
3
+
4
+ describe 'ChefSync::Role' do
5
+
6
+ let(:resource_class) {ChefSync::Role}
7
+
8
+ let(:local_resource) do
9
+ {
10
+ 'name': 'fake_role',
11
+ 'default_attributes': {},
12
+ 'override_attributes': {},
13
+ 'json_class': 'Chef::Role',
14
+ 'description': 'Fake chef role.',
15
+ 'chef_type': 'role',
16
+ 'run_list': [],
17
+ 'env_run_lists': {}
18
+ }
19
+ end
20
+
21
+ let(:remote_resource) do
22
+ local_resource.merge({'description' => 'This is a different fake chef role.'})
23
+ end
24
+
25
+ let(:init_args) { {name: 'fake_role'} }
26
+
27
+ it_should_behave_like 'a chef resource'
28
+
29
+ end
@@ -0,0 +1,86 @@
1
+ require_relative '../spec_helper'
2
+
3
+ RSpec.shared_examples 'a chef resource' do
4
+
5
+ let(:local_knife) do
6
+ local_knife = ChefSync::KnifeMock.new(resource_class.resource_type, :local)
7
+ local_knife.set_success(local_resource)
8
+ local_knife
9
+ end
10
+
11
+ let(:remote_knife) do
12
+ ChefSync::KnifeMock.new(resource_class.resource_type, :remote)
13
+ end
14
+
15
+ let(:dryrun_args) do
16
+ init_args.merge({local_knife: local_knife, remote_knife: remote_knife, dryrun: true})
17
+ end
18
+
19
+ let(:no_dryrun_args) {dryrun_args.merge({dryrun: false})}
20
+
21
+ context 'when the local and remote are the same' do
22
+
23
+ it 'has no required action' do
24
+ remote_knife.set_success(local_resource)
25
+
26
+ resource = resource_class.new(dryrun_args)
27
+ expect(resource).to receive(:upload_resource).never
28
+
29
+ action = resource.sync
30
+ expect(action).to be_a(Symbol)
31
+ expect(action).to eq(:none)
32
+ end
33
+
34
+ end
35
+
36
+
37
+ context 'when the local and remote are different' do
38
+
39
+ it 'needs to be updated' do
40
+ remote_knife.set_success(remote_resource)
41
+
42
+ resource = resource_class.new(dryrun_args)
43
+ expect(resource).to receive(:upload_resource).never
44
+
45
+ action = resource.sync
46
+ expect(action).to be_a(Symbol)
47
+ expect(action).to eq(:update)
48
+ end
49
+
50
+ it 'uploads the changed resource when not in dryrun mode' do
51
+ remote_knife.set_success(remote_resource)
52
+
53
+ resource = resource_class.new(no_dryrun_args)
54
+ expect(resource).to receive(:upload_resource)
55
+ resource.sync
56
+ end
57
+
58
+ end
59
+
60
+
61
+ context 'when the remote does not exist' do
62
+
63
+ it 'needs to be created' do
64
+ error = "ERROR: The object you are looking for could not be found"
65
+ remote_knife.set_error(error, 100)
66
+
67
+ resource = resource_class.new(dryrun_args)
68
+ expect(resource).to receive(:upload_resource).never
69
+
70
+ action = resource.sync
71
+ expect(action).to be_a(Symbol)
72
+ expect(action).to eq(:create)
73
+ end
74
+
75
+ it 'uploads the new resource when not in dryrun mode' do
76
+ error = "ERROR: The object you are looking for could not be found"
77
+ remote_knife.set_error(error, 100)
78
+
79
+ resource = resource_class.new(no_dryrun_args)
80
+ expect(resource).to receive(:upload_resource)
81
+ resource.sync
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,37 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe 'ChefSync::ChefComponent' do
4
+
5
+ let(:local_knife_list) {['badger', 'mushroom', 'snake']}
6
+
7
+ let(:local_knife) do
8
+ local_knife = ChefSync::KnifeMock.new(ChefSync::ChefComponentMock.resource_type, :local)
9
+ local_knife.set_success(local_knife_list)
10
+ local_knife
11
+ end
12
+
13
+ it 'creates an array of formatted summary strings for its resources' do
14
+ expect(ChefSync::ChefComponentMock).to receive(:make_local_knife).and_return(local_knife)
15
+ expect(ChefSync::ChefComponentMock).to receive(:make_remote_knife).and_return(local_knife)
16
+
17
+ result = ChefSync::ChefComponentMock.changes(true)
18
+
19
+ expect(result.count).to eq(3)
20
+ expect(result.first).to include(local_knife_list.first)
21
+ expect(result.last).to include(local_knife_list.last)
22
+ expect(result).to all(include('was updated.'))
23
+ end
24
+
25
+ it 'creates an instance for each local resource and syncs them' do
26
+ expect(ChefSync::ChefComponentMock).to receive(:make_local_knife).and_return(local_knife)
27
+ expect(ChefSync::ChefComponentMock).to receive(:make_remote_knife).and_return(local_knife)
28
+
29
+ enum = ChefSync::ChefComponentMock.each(true)
30
+ resources = [enum.next, enum.next, enum.next]
31
+
32
+ expect(resources.count).to eq(3)
33
+ expect(resources.map(&:name)).to eq(local_knife_list)
34
+ expect(resources.map(&:sync_called?)).to all(be_truthy)
35
+ end
36
+
37
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ChefSync' do
4
+
5
+ before(:all) do
6
+ ChefSync::RESOURCE_TYPES.each {|r| r.total_resources = 3}
7
+ end
8
+
9
+ it 'has no log when there are are no actionable changes or warnings' do
10
+ ChefSync::RESOURCE_TYPES.each {|r| allow(r).to receive(:changes).and_return([])}
11
+
12
+ summ = ChefSync::DRYRUN_MESSAGE.dup
13
+ ChefSync::RESOURCE_TYPES.each do |r|
14
+ summ << "#{r.changes.count}/#{r.total_resources} #{r.resource_type}s have changed. "
15
+ end
16
+
17
+ expect(ChefSync.new.run).to eq([summ, [ChefSync::DEFAULT_LOG_MESSAGE]])
18
+ end
19
+
20
+ it 'has log entries when there is a change' do
21
+ non_cookbooks = [ChefSync::Role, ChefSync::Environment, ChefSync::DataBagItem]
22
+ non_cookbooks.each {|r| allow(r).to receive(:changes).and_return([])}
23
+ cookbook_warning = 'cookbooks/fake_cookbook is newer than the local version.'
24
+ allow(ChefSync::Cookbook).to receive(:changes).and_return([cookbook_warning])
25
+
26
+ summ = ChefSync::DRYRUN_MESSAGE.dup
27
+ ChefSync::RESOURCE_TYPES.each do |r|
28
+ summ << "#{r.changes.count}/#{r.total_resources} #{r.resource_type}s have changed. "
29
+ end
30
+
31
+ expect(ChefSync.new.run).to eq([summ, [cookbook_warning]])
32
+ end
33
+
34
+ end
@@ -0,0 +1,53 @@
1
+ require 'chef_sync'
2
+
3
+ require 'json'
4
+ require 'knife/api'
5
+ require 'rspec'
6
+
7
+ class ChefSync
8
+ class KnifeMock < Knife
9
+
10
+ def set_success(hash)
11
+ @result = [hash.to_json, '', 0]
12
+ end
13
+
14
+ def set_error(err, status)
15
+ @result = ['', err, status]
16
+ end
17
+
18
+ def fork_knife_capture(command, args)
19
+ return @result
20
+ end
21
+
22
+ end
23
+
24
+
25
+ class ChefComponentMock < ChefComponent
26
+
27
+ @resource_type = 'fake_resource'
28
+
29
+ attr_accessor :sync_called
30
+
31
+ def initialize( *args )
32
+ super
33
+ @sync_called = false
34
+ @change = :update
35
+ end
36
+
37
+ def sync
38
+ @sync_called = true
39
+ end
40
+
41
+ def sync_called?
42
+ return @sync_called
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ RSpec.configure do |config|
50
+ config.run_all_when_everything_filtered = true
51
+ config.filter_run :focus
52
+ end
53
+
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chef_synchronize
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.7
5
+ platform: ruby
6
+ authors:
7
+ - Cozy Services Ltd.
8
+ - Rachel King
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-03-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.6'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.6'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.4'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.4'
56
+ - !ruby/object:Gem::Dependency
57
+ name: ridley
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '4.4'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '4.4'
70
+ - !ruby/object:Gem::Dependency
71
+ name: knife-api
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.1'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.1'
84
+ - !ruby/object:Gem::Dependency
85
+ name: slack-post
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.3'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '0.3'
98
+ - !ruby/object:Gem::Dependency
99
+ name: tqdm
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '0.3'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '0.3'
112
+ - !ruby/object:Gem::Dependency
113
+ name: mixlib-versioning
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '1.1'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '1.1'
126
+ description: Sync a monolithic chef repo to a chef server.
127
+ email:
128
+ - opensource@cozy.co
129
+ executables:
130
+ - chef_sync
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - LICENSE.txt
135
+ - README.md
136
+ - bin/chef_sync
137
+ - lib/chef_sync.rb
138
+ - lib/chef_sync/chef_component.rb
139
+ - lib/chef_sync/chef_component/cookbook.rb
140
+ - lib/chef_sync/chef_component/data_bag_item.rb
141
+ - lib/chef_sync/chef_component/environment.rb
142
+ - lib/chef_sync/chef_component/role.rb
143
+ - lib/chef_sync/knife.rb
144
+ - lib/chef_sync/version.rb
145
+ - spec/chef_sync/chef_component/cookbook_spec.rb
146
+ - spec/chef_sync/chef_component/data_bag_item_spec.rb
147
+ - spec/chef_sync/chef_component/environment_spec.rb
148
+ - spec/chef_sync/chef_component/role_spec.rb
149
+ - spec/chef_sync/chef_component_shared_behaviors.rb
150
+ - spec/chef_sync/chef_component_spec.rb
151
+ - spec/chef_sync_spec.rb
152
+ - spec/spec_helper.rb
153
+ homepage: https://github.com/CozyCo/chef_synchronize
154
+ licenses:
155
+ - MIT
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 2.4.5
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: Sync a monolithic chef repo to a chef server.
177
+ test_files:
178
+ - spec/chef_sync/chef_component/cookbook_spec.rb
179
+ - spec/chef_sync/chef_component/data_bag_item_spec.rb
180
+ - spec/chef_sync/chef_component/environment_spec.rb
181
+ - spec/chef_sync/chef_component/role_spec.rb
182
+ - spec/chef_sync/chef_component_shared_behaviors.rb
183
+ - spec/chef_sync/chef_component_spec.rb
184
+ - spec/chef_sync_spec.rb
185
+ - spec/spec_helper.rb