itamae-spec 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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