chef-umami 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef-dk/authenticated_http'
16
+ require 'chef-dk/policyfile/storage_config'
17
+ require 'chef-dk/policyfile/uploader'
18
+ require 'chef-dk/policyfile_lock'
19
+ require 'chef-dk/ui'
20
+
21
+ module Umami
22
+ class Policyfile
23
+ class Uploader
24
+
25
+ attr_reader :http_client
26
+ attr_reader :policyfile_lock
27
+ attr_reader :policyfile_lock_file
28
+ attr_reader :policyfile_uploader
29
+ attr_reader :storage_config
30
+ attr_reader :ui
31
+ def initialize(policyfile_lock_file = nil)
32
+ @http_client = http_client
33
+ @policyfile_lock_file = policyfile_lock_file
34
+ @policyfile_lock = policyfile_lock
35
+ @policyfile_uploader = policyfile_uploader
36
+ @storage_config = storage_config
37
+ @ui = ui
38
+ end
39
+
40
+ def storage_config
41
+ @storage_config ||= ChefDK::Policyfile::StorageConfig.new.use_policyfile(policyfile_lock_file)
42
+ end
43
+
44
+ def ui
45
+ @ui ||= ChefDK::UI.new
46
+ end
47
+
48
+ def policyfile_lock_content
49
+ IO.read(policyfile_lock_file)
50
+ end
51
+
52
+ def lock_data
53
+ FFI_Yajl::Parser.new.parse(policyfile_lock_content)
54
+ end
55
+
56
+ def policyfile_lock
57
+ @policyfile_lock ||= ChefDK::PolicyfileLock.new(
58
+ storage_config,
59
+ ui: ui
60
+ ).build_from_lock_data(lock_data)
61
+ end
62
+
63
+ def http_client
64
+ @http_client ||= ChefDK::AuthenticatedHTTP.new(Chef::Config['chef_server_url'])
65
+ end
66
+
67
+ def policy_group
68
+ Chef::Config['policy_group']
69
+ end
70
+
71
+ def policy_document_native_api
72
+ true
73
+ end
74
+
75
+ def policyfile_uploader
76
+ @policyfile_uploader ||= ChefDK::Policyfile::Uploader.new(
77
+ policyfile_lock,
78
+ policy_group,
79
+ ui: ui,
80
+ http_client: http_client,
81
+ policy_document_native_api: policy_document_native_api
82
+ )
83
+ end
84
+
85
+ # Push the policy, including all dependent cookbooks.
86
+ def upload
87
+ policyfile_uploader.upload
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef'
16
+ require 'chef-umami/exceptions'
17
+ require 'chef-umami/client'
18
+ require 'chef-umami/logger'
19
+ require 'chef-umami/server'
20
+ require 'chef-umami/policyfile/exporter'
21
+ require 'chef-umami/policyfile/uploader'
22
+ require 'chef-umami/test/unit'
23
+ require 'chef-umami/test/integration'
24
+
25
+ module Umami
26
+ class Runner
27
+
28
+ include Umami::Logger
29
+
30
+ attr_reader :cookbook_dir
31
+ attr_reader :policyfile_lock_file
32
+ attr_reader :policyfile
33
+ # TODO: Build the ability to specify a custom policy lock file name.
34
+ def initialize(policyfile_lock_file = nil, policyfile = nil)
35
+ @cookbook_dir = Dir.pwd
36
+ @policyfile_lock_file = 'Policyfile.lock.json'
37
+ @policyfile = policyfile || 'Policyfile.rb'
38
+ @exporter = exporter
39
+ @chef_zero_server = chef_zero_server
40
+ # If we load the uploader or client now, they won't see the updated
41
+ # Chef config!
42
+ @uploader = nil
43
+ @chef_client = nil
44
+ end
45
+
46
+ def validate_lock_file!
47
+ unless policyfile_lock_file.end_with?("lock.json")
48
+ raise InvalidPolicyfileLockFilename, "Policyfile lock files must end in '.lock.json'. I received '#{policyfile_lock_file}'."
49
+ end
50
+
51
+ unless File.exist?(policyfile_lock_file)
52
+ raise InvalidPolicyfileLockFilename, "Unable to locate '#{policyfile_lock_file}' You may need to run `chef install` to generate it."
53
+ end
54
+ end
55
+
56
+ def exporter
57
+ @exporter ||= Umami::Policyfile::Exporter.new(policyfile_lock_file, cookbook_dir, policyfile)
58
+ end
59
+
60
+ def uploader
61
+ @uploader ||= Umami::Policyfile::Uploader.new(policyfile_lock_file)
62
+ end
63
+
64
+ def chef_zero_server
65
+ @chef_zero_server ||= Umami::Server.new
66
+ end
67
+
68
+ def chef_client
69
+ @chef_client ||= Umami::Client.new
70
+ end
71
+
72
+ def run
73
+ validate_lock_file!
74
+ puts "\nExporting the policy, related cookbooks, and a valid client configuration..."
75
+ exporter.export
76
+ Chef::Config.from_file("#{exporter.chef_config_file}")
77
+ chef_zero_server.start
78
+ puts "\nUploading the policy and related cookbooks..."
79
+ uploader.upload
80
+ puts "\nExecuting chef-client compile phase..."
81
+ # Define Chef::Config['config_file'] lest Ohai complain.
82
+ Chef::Config['config_file'] = exporter.chef_config_file
83
+ chef_client.compile
84
+ # Build a hash of all the recipes' resources, keyed by the canonical
85
+ # name of the recipe (i.e. ohai::default).
86
+ recipe_resources = {}
87
+ chef_client.resource_collection.each do |resource|
88
+ canonical_recipe = "#{resource.cookbook_name}::#{resource.recipe_name}"
89
+ if recipe_resources.key?(canonical_recipe)
90
+ recipe_resources[canonical_recipe] << resource
91
+ else
92
+ recipe_resources[canonical_recipe] = [resource]
93
+ end
94
+ end
95
+
96
+ # Remove the temporary directory using a naive guard to ensure we're
97
+ # deleting what we expect.
98
+ re_export_path = Regexp.new('/tmp/umami')
99
+ FileUtils.rm_rf(exporter.export_root) if exporter.export_root.match(re_export_path)
100
+
101
+ puts "\nGenerating a set of unit tests..."
102
+ unit_tester = Umami::Test::Unit.new
103
+ unit_tester.generate(recipe_resources)
104
+
105
+ puts "\nGenerating a set of integration tests..."
106
+ integration_tester = Umami::Test::Integration.new
107
+ integration_tester.generate(recipe_resources)
108
+
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef_zero/server'
16
+
17
+ module Umami
18
+ class Server
19
+ def initialize
20
+ @server = server
21
+ end
22
+
23
+ def server
24
+ @server ||= ChefZero::Server.new(port: 8889)
25
+ end
26
+
27
+ def start
28
+ server.start_background
29
+ end
30
+
31
+ def stop
32
+ server.stop
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Umami
16
+ class Test
17
+
18
+ attr_reader :root_dir
19
+ def initialize
20
+ @root_dir = 'spec'
21
+ end
22
+
23
+ # All subclasses should implement the following methods.
24
+
25
+ # #framework should return a string describing the framework it's
26
+ # expected to write tests for.
27
+ # Examples:
28
+ # "chefspec"
29
+ # "serverspec"
30
+ # "inspec"
31
+ def framework
32
+ raise NoMethodError, "#{self.class} needs to implement the ##{__method__} method! Refer to Umami::Test."
33
+ end
34
+
35
+ # #preamble should return a string (with newlines) that will appear at
36
+ # the top of a test file.
37
+ # Expects a string representing the recipe name, at least.
38
+ # Example:
39
+ # "# #{test_root}/#{recipe}_spec.rb\n" \
40
+ # "\n" \
41
+ # "require '#{framework}'\n" \
42
+ # "\n" \
43
+ # "describe '#{recipe}' do\n" \
44
+ # " let(:chef_run) { ChefSpec::ServerRunner.converge(described_recipe) }"
45
+ def preamble(recipe = '')
46
+ raise NoMethodError, "#{self.class} needs to implement the ##{__method__} method! Refer to Umami::Test."
47
+ end
48
+
49
+ # #write_test should write a single, discreet test for a given resource.
50
+ # Return as a string with newlines.
51
+ # Expects a Chef::Resource object.
52
+ def write_test(resource = nil)
53
+ raise NoMethodError, "#{self.class} needs to implement the ##{__method__} method! Refer to Umami::Test."
54
+ end
55
+
56
+ # Performs the necessary steps to generate one or more tests within a
57
+ # test file.
58
+ def generate
59
+ raise NoMethodError, "#{self.class} needs to implement the ##{__method__} method! Refer to Umami::Test."
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,95 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef-umami/test'
16
+ require 'chef-umami/helpers/inspec'
17
+ require 'chef-umami/helpers/filetools'
18
+
19
+ module Umami
20
+ class Test
21
+ class Integration < Umami::Test
22
+
23
+ include Umami::Helper::InSpec
24
+ include Umami::Helper::FileTools
25
+
26
+ attr_reader :test_root
27
+ def initialize
28
+ super
29
+ @test_root = File.join(self.root_dir, 'umami', 'integration')
30
+ end
31
+
32
+ # InSpec doesn't need a require statement to use its tests.
33
+ # We define #framework here for completeness.
34
+ def framework
35
+ "inspec"
36
+ end
37
+
38
+ def test_file(cookbook = '', recipe = '')
39
+ "#{test_root}/#{cookbook}_#{recipe}_spec.rb"
40
+ end
41
+
42
+ def preamble(cookbook = '', recipe = '')
43
+ "# #{test_file(cookbook, recipe)} - Originally written by Umami!"
44
+ end
45
+
46
+ # Call on the apprpriate method from the Umami::Helper::InSpec
47
+ # module to generate our test.
48
+ def write_test(resource = nil)
49
+ if resource.action.is_a? Array
50
+ return if resource.action.include?(:delete)
51
+ end
52
+ return if resource.action == :delete
53
+ "\n" + send("test_#{resource.declared_type}", resource)
54
+ end
55
+
56
+ # If the test framework's helper module doesn't provide support for a
57
+ # given test-related method, return a friendly message.
58
+ # Raise NoMethodError for any other failed calls.
59
+ def method_missing(m, *args, &block)
60
+ case m
61
+ when /^test_/
62
+ "# #{m} is not currently defined. Stay tuned for updates."
63
+ else
64
+ raise NoMethodError
65
+ end
66
+ end
67
+
68
+ def generate(recipe_resources = {})
69
+ test_files_written = []
70
+ recipe_resources.each do |canonical_recipe, resources|
71
+ (cookbook, recipe) = canonical_recipe.split('::')
72
+ content = [preamble(cookbook, recipe)]
73
+ resources.each do |resource|
74
+ content << write_test(resource)
75
+ end
76
+ test_file_name = test_file(cookbook, recipe)
77
+ test_file_content = content.join("\n") + "\n"
78
+ write_file(test_file_name, test_file_content)
79
+ test_files_written << test_file_name
80
+ end
81
+
82
+ enforce_styling(test_root)
83
+
84
+ unless test_files_written.empty?
85
+ puts "Wrote the following integration tests:"
86
+ test_files_written.each do |f|
87
+ puts "\t#{f}"
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,108 @@
1
+ # Copyright 2017 Bloomberg Finance, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef-umami/test'
16
+ require 'chef-umami/helpers/os'
17
+ require 'chef-umami/helpers/filetools'
18
+
19
+ module Umami
20
+ class Test
21
+ class Unit < Umami::Test
22
+
23
+ include Umami::Helper::OS
24
+ include Umami::Helper::FileTools
25
+
26
+ attr_reader :test_root
27
+ attr_reader :tested_cookbook # This cookbook.
28
+ def initialize
29
+ super
30
+ @test_root = File.join(self.root_dir, 'umami', 'unit', 'recipes')
31
+ @tested_cookbook = File.basename(Dir.pwd)
32
+ end
33
+
34
+ def framework
35
+ "chefspec"
36
+ end
37
+
38
+ def test_file(recipe = '')
39
+ "#{test_root}/#{recipe}_spec.rb"
40
+ end
41
+
42
+ def preamble(cookbook = '', recipe = '')
43
+ "# #{test_file(recipe)} - Originally written by Umami!\n" \
44
+ "\n" \
45
+ "require '#{framework}'\n" \
46
+ "require '#{framework}/policyfile'\n" \
47
+ "\n" \
48
+ "describe '#{cookbook}::#{recipe}' do\n" \
49
+ "let(:chef_run) { ChefSpec::ServerRunner.new(platform: '#{os[:platform]}', version: '#{os[:version]}').converge(described_recipe) }"
50
+ end
51
+
52
+ def write_test(resource = nil)
53
+ state_attrs = [] # Attribute hash to be used with #with()
54
+ resource.state.each do |attr, value|
55
+ next if value.nil? or (value.respond_to?(:empty) and value.empty?)
56
+ if value.is_a? String
57
+ value = value.gsub("'", "\\\\'") # Escape any single quotes in the value.
58
+ end
59
+ state_attrs << "#{attr}: '#{value}'"
60
+ end
61
+ action = ''
62
+ if resource.action.is_a? Array
63
+ action = resource.action.first
64
+ else
65
+ action = resource.action
66
+ end
67
+ resource_name = resource.name.gsub("'", "\\\\'") # Escape any single quotes in the resource name.
68
+ test_output = ["\nit '#{action}s #{resource.declared_type} \"#{resource_name}\"' do"]
69
+ if state_attrs.empty?
70
+ test_output << "expect(chef_run).to #{action}_#{resource.declared_type}('#{resource_name}')"
71
+ else
72
+ test_output << "expect(chef_run).to #{action}_#{resource.declared_type}('#{resource_name}').with(#{state_attrs.join(', ')})"
73
+ end
74
+ test_output << "end\n"
75
+ test_output.join("\n")
76
+ end
77
+
78
+ def generate(recipe_resources = {})
79
+ test_files_written = []
80
+ recipe_resources.each do |canonical_recipe, resources|
81
+ (cookbook, recipe) = canonical_recipe.split('::')
82
+ # Only write unit tests for the cookbook we're in.
83
+ next unless cookbook == tested_cookbook
84
+ content = [preamble(cookbook, recipe)]
85
+ resources.each do |resource|
86
+ content << write_test(resource)
87
+ end
88
+ content << "end"
89
+ test_file_name = test_file(recipe)
90
+ test_file_content = content.join("\n") + "\n"
91
+ write_file(test_file_name, test_file_content)
92
+ test_files_written << test_file_name
93
+ end
94
+
95
+ enforce_styling(test_root)
96
+
97
+ unless test_files_written.empty?
98
+ puts "Wrote the following unit test files:"
99
+ test_files_written.each do |f|
100
+ puts "\t#{f}"
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+ end
108
+ end