itamae-spec 0.0.2

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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +42 -0
  7. data/Rakefile +47 -0
  8. data/bin/itamae-spec +4 -0
  9. data/itamae-spec.gemspec +36 -0
  10. data/lib/itamae-spec.rb +10 -0
  11. data/lib/itamae-spec/cli.rb +19 -0
  12. data/lib/itamae-spec/generators.rb +20 -0
  13. data/lib/itamae-spec/generators/cookbook.rb +10 -0
  14. data/lib/itamae-spec/generators/project.rb +10 -0
  15. data/lib/itamae-spec/generators/templates/cookbook/attributes/.keep +0 -0
  16. data/lib/itamae-spec/generators/templates/cookbook/recipes/default.rb +0 -0
  17. data/lib/itamae-spec/generators/templates/cookbook/recipes/files/.keep +0 -0
  18. data/lib/itamae-spec/generators/templates/cookbook/recipes/templates/.keep +0 -0
  19. data/lib/itamae-spec/generators/templates/cookbook/spec/default_spec.rb +0 -0
  20. data/lib/itamae-spec/generators/templates/project/.rspec +2 -0
  21. data/lib/itamae-spec/generators/templates/project/Gemfile +3 -0
  22. data/lib/itamae-spec/generators/templates/project/Project.json +1 -0
  23. data/lib/itamae-spec/generators/templates/project/Rakefile +9 -0
  24. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/attributes/.keep +0 -0
  25. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/attributes/default.json +5 -0
  26. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/recipes/default.rb +7 -0
  27. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/recipes/files/.keep +0 -0
  28. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/recipes/templates/.keep +0 -0
  29. data/lib/itamae-spec/generators/templates/project/cookbooks/sample/spec/default_spec.rb +9 -0
  30. data/lib/itamae-spec/generators/templates/project/environments/.keep +0 -0
  31. data/lib/itamae-spec/generators/templates/project/environments/sample.json +7 -0
  32. data/lib/itamae-spec/generators/templates/project/keys/.keep +0 -0
  33. data/lib/itamae-spec/generators/templates/project/nodes/.keep +0 -0
  34. data/lib/itamae-spec/generators/templates/project/nodes/sample.json +10 -0
  35. data/lib/itamae-spec/generators/templates/project/roles/.keep +0 -0
  36. data/lib/itamae-spec/generators/templates/project/roles/sample.json +5 -0
  37. data/lib/itamae-spec/generators/templates/project/spec/spec_helper.rb +41 -0
  38. data/lib/itamae-spec/generators/templates/project/tmp-nodes/.keep +0 -0
  39. data/lib/itamae-spec/logger.rb +76 -0
  40. data/lib/itamae-spec/resource.rb +2 -0
  41. data/lib/itamae-spec/resource/http_request.rb +71 -0
  42. data/lib/itamae-spec/resource/s3_file.rb +31 -0
  43. data/lib/itamae-spec/task/base.rb +90 -0
  44. data/lib/itamae-spec/task/base_task.rb +148 -0
  45. data/lib/itamae-spec/task/itamae_task.rb +112 -0
  46. data/lib/itamae-spec/task/local_itamae_task.rb +84 -0
  47. data/lib/itamae-spec/task/local_serverspec_task.rb +125 -0
  48. data/lib/itamae-spec/task/serverspec_task.rb +111 -0
  49. data/lib/itamae-spec/version.rb +3 -0
  50. data/lib/itamae-spec/version.txt +1 -0
  51. data/spec/integration/Vagrantfile +35 -0
  52. data/spec/integration/default_spec.rb +226 -0
  53. data/spec/integration/recipes/default.rb +423 -0
  54. data/spec/integration/recipes/default2.rb +6 -0
  55. data/spec/integration/recipes/define/default.rb +6 -0
  56. data/spec/integration/recipes/define/files/remote_file_in_definition +1 -0
  57. data/spec/integration/recipes/dry_run.rb +6 -0
  58. data/spec/integration/recipes/files/remote_file_auto +1 -0
  59. data/spec/integration/recipes/hello.erb +6 -0
  60. data/spec/integration/recipes/hello.txt +1 -0
  61. data/spec/integration/recipes/included.rb +9 -0
  62. data/spec/integration/recipes/node.json +3 -0
  63. data/spec/integration/recipes/redefine.rb +20 -0
  64. data/spec/integration/recipes/templates/template_auto.erb +6 -0
  65. data/spec/integration/spec_helper.rb +42 -0
  66. data/spec/unit/lib/itamae/backend_spec.rb +95 -0
  67. data/spec/unit/lib/itamae/handler/base_spec.rb +34 -0
  68. data/spec/unit/lib/itamae/handler/fluentd_spec.rb +19 -0
  69. data/spec/unit/lib/itamae/handler_proxy_spec.rb +38 -0
  70. data/spec/unit/lib/itamae/handler_spec.rb +11 -0
  71. data/spec/unit/lib/itamae/node_spec.rb +14 -0
  72. data/spec/unit/lib/itamae/recipe_spec.rb +6 -0
  73. data/spec/unit/lib/itamae/resource/base_spec.rb +127 -0
  74. data/spec/unit/lib/itamae/resource_spec.rb +23 -0
  75. data/spec/unit/lib/itamae/runner_spec.rb +32 -0
  76. data/spec/unit/spec_helper.rb +23 -0
  77. metadata +315 -0
@@ -0,0 +1,2 @@
1
+ require 'itamae-spec/resource/http_request'
2
+ require 'itamae-spec/resource/s3_file'
@@ -0,0 +1,71 @@
1
+
2
+ module Itamae
3
+ module Resource
4
+ class HttpRequest
5
+ def pre_action
6
+ attributes.content = fetch_content
7
+ current.exist = run_specinfra(:check_file_is_file, attributes.path)
8
+ attributes.exist = true
9
+
10
+ send_tempfile
11
+ compare_file
12
+ end
13
+
14
+ def show_differences
15
+ current.mode = current.mode.rjust(4, '0') if current.mode
16
+ attributes.mode = attributes.mode.rjust(4, '0') if attributes.mode
17
+
18
+ @current_attributes.each_pair do |key, current_value|
19
+ value = @attributes[key]
20
+ if current_value.nil? && value.nil?
21
+ # ignore
22
+ elsif current_value.nil? && !value.nil?
23
+ Itamae.logger.color :green do
24
+ Itamae.logger.info "#{resource_type}[#{resource_name}] #{key} will be '#{value}'"
25
+ end
26
+ elsif current_value == value || value.nil?
27
+ Itamae.logger.debug "#{resource_type}[#{resource_name}] #{key} will not change (current value is '#{current_value}')"
28
+ else
29
+ Itamae.logger.color :green do
30
+ Itamae.logger.info "#{resource_type}[#{resource_name}] #{key} will change from '#{current_value}' to '#{value}'"
31
+ end
32
+ end
33
+ end
34
+
35
+ show_content_diff
36
+ end
37
+
38
+ def fetch_content
39
+ uri = URI.parse(attributes.url)
40
+ response = nil
41
+ redirects_followed = 0
42
+
43
+ loop do
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+ http.use_ssl = true if uri.scheme == "https"
46
+
47
+ case @current_action
48
+ when :delete, :get, :options
49
+ response = http.method(@current_action).call(uri.request_uri, attributes.headers)
50
+ when :post, :put
51
+ response = http.method(@current_action).call(uri.request_uri, attributes.message, attributes.headers)
52
+ end
53
+
54
+ if response.kind_of?(Net::HTTPRedirection)
55
+ if redirects_followed < attributes.redirect_limit
56
+ uri = URI.parse(response["location"])
57
+ redirects_followed += 1
58
+ ItamaeMitsurin.logger.debug "Following redirect #{redirects_followed}/#{attributes.redirect_limit}"
59
+ else
60
+ raise RedirectLimitExceeded
61
+ end
62
+ else
63
+ break
64
+ end
65
+ end
66
+
67
+ response.body
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ require 'aws-sdk'
2
+
3
+ module Itamae
4
+ module Resource
5
+ class S3File < File
6
+ define_attribute :object_key, type: String, default_name: true
7
+ define_attribute :region, type: String, required: true
8
+ define_attribute :bucket, type: String, required: true
9
+ define_attribute :profile, type: String, default: 'default'
10
+
11
+ private
12
+
13
+ def pre_action
14
+ credentials = Aws::SharedCredentials.new(profile_name: attributes.profile)
15
+ @s3 = Aws::S3::Client.new(region: attributes.region, credentials: credentials)
16
+ attributes.content = fetch_content
17
+
18
+ super
19
+ end
20
+
21
+ def fetch_content
22
+ case @current_action
23
+ when :create, :delete, :edit
24
+ resp = @s3.get_object(bucket: attributes.bucket, key: attributes.object_key)
25
+ end
26
+
27
+ resp.body.read
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,90 @@
1
+ require 'multi_json'
2
+ require 'itamae'
3
+
4
+ module ItamaeSpec
5
+ module Task
6
+ module Base
7
+ RoleLoadError = Class.new(StandardError)
8
+
9
+ class ::Hash
10
+ def deep_merge(other_hash, &block)
11
+ dup.deep_merge!(other_hash, &block)
12
+ end
13
+
14
+ def deep_merge!(other_hash, &block)
15
+ merge!(other_hash) do |key, this_val, other_val|
16
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
17
+ this_val.deep_merge(other_val, &block)
18
+ elsif block_given?
19
+ yield(key, this_val, other_val)
20
+ else
21
+ other_val
22
+ end
23
+ end
24
+ end
25
+
26
+ def to_pretty_json
27
+ MultiJson.dump(self, symbolize_keys: true, pretty: true)
28
+ end
29
+ end
30
+
31
+ class ::Regexp
32
+ def match?(m)
33
+ if self.match(m)
34
+ true
35
+ else
36
+ false
37
+ end
38
+ end
39
+ end
40
+
41
+ class << self
42
+ def get_role_recipes(role)
43
+ recipes = []
44
+ JSON.parse(File.read("roles/#{role}.json"))['run_list'].each do |recipe|
45
+ if /recipe\[(.+)::(.+)\]/.match?(recipe)
46
+ recipes << { recipe.gsub(/recipe\[(.+)::(.+)\]/, '\1') => recipe.gsub(/recipe\[(.+)::(.+)\]/, '\2') }
47
+ elsif /recipe\[(.+)\]/.match?(recipe)
48
+ recipes << { recipe.gsub(/recipe\[(.+)\]/, '\1') => 'default' }
49
+ end
50
+ end
51
+ rescue JSON::ParserError
52
+ raise RoleLoadError, "JSON Parser Faild. - roles/#{role}.json"
53
+ rescue Errno::ENOENT
54
+ raise RoleLoadError, "No such role file or directory - roles/#{role}.json"
55
+ else
56
+ recipes
57
+ end
58
+
59
+ def get_node_recipes(node_file)
60
+ recipes = []
61
+ JSON.parse(File.read(node_file))['run_list'].each do |recipe|
62
+ if /recipe\[(.+)::(.+)\]/.match?(recipe)
63
+ recipes << { recipe.gsub(/recipe\[(.+)::(.+)\]/, '\1') => recipe.gsub(/recipe\[(.+)::(.+)\]/, '\2') }
64
+ elsif /recipe\[(.+)\]/.match?(recipe)
65
+ recipes << { recipe.gsub(/recipe\[(.+)\]/, '\1') => 'default' }
66
+ elsif /role\[(.+)\]/.match?(recipe)
67
+ recipes << get_role_recipes(recipe.gsub(/role\[(.+)\]/, '\1'))
68
+ end
69
+ end
70
+ rescue JSON::ParserError
71
+ raise RoleLoadError, "JSON Parser Faild. - #{node_file}"
72
+ rescue Errno::ENOENT
73
+ raise RoleLoadError, "No such node file or directory - #{node_fie}"
74
+ else
75
+ recipes
76
+ end
77
+
78
+ def write_tmp_nodes(filename)
79
+ Itamae.logger.info "Output attributes log file to: tmp-nodes/#{filename}.json"
80
+
81
+ File.open "tmp-nodes/#{filename}.json", 'w' do |f|
82
+ f.flock File::LOCK_EX
83
+ yield f
84
+ f.flock File::LOCK_UN
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,148 @@
1
+ require 'itamae-spec/task/base'
2
+
3
+ module ItamaeSpec
4
+ module Task
5
+ class BaseTask
6
+ extend Rake::DSL if defined? Rake::DSL
7
+
8
+ EnvironmentsSetError = Class.new(StandardError)
9
+ LoadRecipeError = Class.new(StandardError)
10
+ LoadAttributeError = Class.new(StandardError)
11
+
12
+ def load_node_attributes(node_file)
13
+ JSON.parse(File.read(node_file), symbolize_names: true)
14
+ rescue JSON::ParserError
15
+ raise LoadAttributeError, "JSON Parser Failed. - #{node_file}"
16
+ end
17
+
18
+ def load_run_list(node_file)
19
+ run_list = []
20
+ Base.get_node_recipes(node_file).each {|recipe| run_list << recipe }
21
+ run_list.flatten
22
+ end
23
+
24
+ def load_environments(hash)
25
+ set = hash[:environments][:set]
26
+ raise EnvironmentsSetError, 'Environments Set is not specified in nodefile' if set.nil?
27
+ JSON.parse(File.read("environments/#{set}.json"), symbolize_names: true)
28
+ rescue JSON::ParserError
29
+ raise LoadAttributeError, "JSON Parser Failed. - environments/#{set}.json"
30
+ end
31
+
32
+ def load_recipe_attributes(run_list)
33
+ recipe_files = run_list.map do |recipe|
34
+ Dir.glob("cookbooks/**/#{recipe.keys.join}/attributes/#{recipe.values.join}.json")
35
+ end.flatten
36
+
37
+ recipe_files.map do |f|
38
+ begin
39
+ JSON.parse(File.read(f), symbolize_names: true)
40
+ rescue JSON::ParserError
41
+ raise LoadAttributeError, "JSON Parser Failed. - #{f}"
42
+ end
43
+ end
44
+ end
45
+
46
+ def merge_attributes(source, other = nil)
47
+ if source.class == Hash
48
+ merged = source.deep_merge(other)
49
+ elsif source.class == Array
50
+ if source.empty?
51
+ merged = {}
52
+ else
53
+ merged = source[0]
54
+ source.each {|s| merged.deep_merge!(s) }
55
+ end
56
+ end
57
+
58
+ merged
59
+ end
60
+
61
+ def create_tmp_nodes(filename, hash)
62
+ json = hash.to_pretty_json
63
+ Base.write_tmp_nodes(filename) {|f| f.puts json }
64
+ end
65
+
66
+ def create_itamae_command(node_name, hash)
67
+ ENV['SUDO_PASSWORD'] if hash[:environments][:sudo_password]
68
+
69
+ command = 'bundle exec itamae-spec ssh'
70
+ command << if hash[:environments][:local_ipv4]
71
+ " -h #{hash[:environments][:local_ipv4]}"
72
+ else
73
+ " -h #{hash[:environments][:hostname]}"
74
+ end
75
+
76
+ command << " -u #{hash[:environments][:ssh_user]}"
77
+ command << " -p #{hash[:environments][:ssh_port]}"
78
+ command << " -i keys/#{hash[:environments][:ssh_key]}" if hash[:environments][:ssh_key]
79
+ command << " -j tmp-nodes/#{node_name}.json"
80
+
81
+ hash[:environments][:shell] = ENV['shell'] if ENV['shell']
82
+ command << if hash[:environments][:shell]
83
+ " --shell=#{hash[:environments][:shell]}"
84
+ else
85
+ ' --shell=bash'
86
+ end
87
+
88
+ command << " --password=#{hash[:environments][:ssh_password]}" if hash[:environments][:ssh_password]
89
+ command << ' --dry-run' if ENV['dry-run'] == 'true'
90
+ command << ' --log-level=debug' if ENV['debug'] == 'true'
91
+ command << ' --vagrant' if ENV['vagrant'] == 'true'
92
+ command
93
+ end
94
+
95
+ def create_spec_command(node_name, hash)
96
+ ENV['TARGET_HOST'] = if hash[:environments][:local_ipv4].nil?
97
+ hash[:environments][:hostname]
98
+ else
99
+ hash[:environments][:local_ipv4]
100
+ end
101
+
102
+ ENV['NODE_FILE'] = "tmp-nodes/#{node_name}.json"
103
+ ENV['SSH_PASSWORD'] = hash[:environments][:ssh_password]
104
+ ENV['SUDO_PASSWORD'] = hash[:environments][:sudo_password]
105
+ ENV['SSH_KEY'] = "keys/#{hash[:environments][:ssh_key]}"
106
+ ENV['SSH_USER'] = hash[:environments][:ssh_user]
107
+ ENV['SSH_PORT'] = hash[:environments][:ssh_port]
108
+
109
+ command = 'bundle exec rspec'
110
+ # ENV['vagrant'] TODO
111
+ end
112
+
113
+ def list_recipe_filepath(run_list)
114
+ recipes = []
115
+ run_list.each do |recipe|
116
+ target_list = Dir.glob("cookbooks/**/#{recipe.keys.join}/recipes/#{recipe.values.join}.rb")
117
+
118
+ raise LoadRecipeError, "#{recipe.to_a.join('::')} cookbook or recipe does not exist." if target_list.empty?
119
+
120
+ target_list.each do |target|
121
+ recipes << " #{target}"
122
+ end
123
+ end
124
+
125
+ recipes
126
+ end
127
+
128
+ def runner_display(raw_run_list, run_list, command)
129
+ run_list_str = run_list.map do |recipe|
130
+ if recipe.values.join == 'default'
131
+ recipe.keys.join
132
+ else
133
+ "#{recipe.keys.join}::#{recipe.values.join}"
134
+ end
135
+ end
136
+
137
+ Itamae.logger.color(:green) do
138
+ Itamae.logger.info "Run List is [#{raw_run_list.join(', ')}]"
139
+ Itamae.logger.info "Run List expands to [#{run_list_str.join(', ')}]"
140
+ end
141
+
142
+ Itamae.logger.color(:white) do
143
+ Itamae.logger.info command
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,112 @@
1
+ require 'itamae-spec/task/base_task'
2
+
3
+ module ItamaeSpec
4
+ module Task
5
+ class ItamaeTask < BaseTask
6
+ ChangeTargetError = Class.new(StandardError)
7
+
8
+ def list_recipe_filepath(run_list)
9
+ recipes = []
10
+ run_list.each do |recipe|
11
+ target_list = Dir.glob("cookbooks/#{recipe.keys.join}/recipes/#{recipe.values.join}.rb")
12
+
13
+ raise LoadRecipeError, "#{recipe.to_a.join('::')} cookbook or recipe does not exist." if target_list.empty?
14
+
15
+ target_list.each do |target|
16
+ recipes << " #{target}"
17
+ end
18
+ end
19
+
20
+ recipes
21
+ end
22
+
23
+ Itamae.logger.formatter.colored = true
24
+ task = ItamaeTask.new
25
+
26
+ namespace :itamae do
27
+ all = []
28
+
29
+ begin
30
+ project = { project: ARGV[1] }
31
+
32
+ if (ARGV[0] == '-T' || ARGV[0] == '--tasks') && !project[:project].nil?
33
+ unless Dir.exist?("nodes/#{project[:project]}")
34
+ raise ChangeTargetError, "'#{project[:project]}' project is not exist."
35
+ end
36
+
37
+ File.open 'Project.json', 'w' do |f|
38
+ f.flock File::LOCK_EX
39
+ f.puts project.to_json
40
+ f.flock File::LOCK_UN
41
+ end
42
+
43
+ Itamae.logger.color(:green) do
44
+ Itamae.logger.info "Changed target mode '#{project[:project]}'"
45
+ end
46
+ end
47
+
48
+ resp = JSON.parse(File.read('Project.json'))
49
+ target = resp['project'] << '/**'
50
+ rescue Errno::ENOENT
51
+ Itamae.logger.error 'Please select target. - ex: $ rake -T .'
52
+ rescue => e
53
+ Itamae.logger.error e.inspect
54
+ exit 2
55
+ end
56
+
57
+ Dir.glob("nodes/#{target}/*.json").each do |node_file|
58
+ begin
59
+ node_name = File.basename(node_file, '.json')
60
+ node = task.load_node_attributes(node_file)
61
+ node_short = node[:environments][:hostname].split('.')[0]
62
+ rescue => e
63
+ Itamae.logger.error e.inspect
64
+ Itamae.logger.info "From node file: #{node_file}"
65
+ exit 2
66
+ end
67
+
68
+ all << node_short
69
+ desc 'Itamae to all nodes'
70
+ task 'all' => all
71
+
72
+ desc "Itamae to #{node_name}"
73
+ task node_short do
74
+ Itamae.logger.color(:cyan) do
75
+ Itamae.logger.info "Start itamae_task to #{node[:environments][:hostname]}"
76
+ end
77
+
78
+ begin
79
+ run_list = task.load_run_list(node_file)
80
+ environments = task.load_environments(node)
81
+ recipe_attributes_list = task.load_recipe_attributes(run_list)
82
+
83
+ merged_recipe = task.merge_attributes(recipe_attributes_list)
84
+ merged_environments = task.merge_attributes(merged_recipe, environments)
85
+ attributes = task.merge_attributes(merged_environments, node)
86
+ task.create_tmp_nodes(node_name, attributes)
87
+
88
+ command = task.create_itamae_command(node_name, attributes)
89
+ command_recipe = task.list_recipe_filepath(run_list)
90
+ command << command_recipe.join
91
+
92
+ task.runner_display(attributes[:run_list], run_list, command)
93
+ st = system command
94
+ if st
95
+ Itamae.logger.color(:green) do
96
+ Itamae.logger.info 'itamae_task is completed.'
97
+ end
98
+ else
99
+ Itamae.logger.error 'itamae_task is failed.'
100
+ exit 1
101
+ end
102
+ rescue => e
103
+ Itamae.logger.error e.inspect
104
+ Itamae.logger.info "From node file: #{node_file}"
105
+ exit 2
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end