itamae 1.10.0 → 1.12.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +203 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +144 -1
  5. data/README.md +2 -3
  6. data/Rakefile +50 -42
  7. data/bin/itamae +0 -1
  8. data/itamae.gemspec +2 -2
  9. data/lib/itamae/backend.rb +20 -5
  10. data/lib/itamae/cli.rb +14 -9
  11. data/lib/itamae/definition.rb +0 -2
  12. data/lib/itamae/logger.rb +5 -1
  13. data/lib/itamae/mash.rb +7 -0
  14. data/lib/itamae/node.rb +4 -4
  15. data/lib/itamae/notification.rb +0 -2
  16. data/lib/itamae/recipe.rb +20 -3
  17. data/lib/itamae/resource/base.rb +3 -3
  18. data/lib/itamae/resource/directory.rb +0 -2
  19. data/lib/itamae/resource/execute.rb +0 -2
  20. data/lib/itamae/resource/file.rb +43 -6
  21. data/lib/itamae/resource/gem_package.rb +0 -2
  22. data/lib/itamae/resource/git.rb +5 -7
  23. data/lib/itamae/resource/group.rb +0 -2
  24. data/lib/itamae/resource/http_request.rb +12 -3
  25. data/lib/itamae/resource/link.rb +0 -2
  26. data/lib/itamae/resource/local_ruby_block.rb +0 -2
  27. data/lib/itamae/resource/package.rb +3 -3
  28. data/lib/itamae/resource/remote_directory.rb +1 -3
  29. data/lib/itamae/resource/remote_file.rb +0 -2
  30. data/lib/itamae/resource/service.rb +0 -2
  31. data/lib/itamae/resource/template.rb +8 -4
  32. data/lib/itamae/resource/user.rb +0 -2
  33. data/lib/itamae/resource.rb +0 -1
  34. data/lib/itamae/runner.rb +3 -4
  35. data/lib/itamae/version.rb +1 -1
  36. data/lib/itamae.rb +1 -1
  37. data/spec/integration/default_spec.rb +26 -32
  38. data/spec/integration/docker_spec.rb +29 -0
  39. data/spec/integration/local_spec.rb +6 -0
  40. data/spec/integration/ordinary_user_spec.rb +108 -0
  41. data/spec/integration/recipes/default.rb +10 -48
  42. data/spec/integration/recipes/docker.rb +44 -0
  43. data/spec/integration/recipes/local.rb +19 -0
  44. data/spec/integration/recipes/ordinary_user.rb +109 -0
  45. data/spec/integration/recipes/toplevel_module.rb +6 -0
  46. data/spec/integration/recipes/variables.rb +14 -0
  47. data/spec/unit/lib/itamae/backend_spec.rb +10 -10
  48. data/tasks/integration_local_spec.rb +117 -0
  49. metadata +26 -9
  50. data/.travis.yml +0 -39
@@ -0,0 +1,7 @@
1
+ require 'hashie'
2
+
3
+ module Itamae
4
+ class Mash < Hashie::Mash
5
+ disable_warnings
6
+ end
7
+ end
data/lib/itamae/node.rb CHANGED
@@ -1,7 +1,7 @@
1
- require 'itamae'
2
1
  require 'hashie'
3
2
  require 'json'
4
3
  require 'schash'
4
+ require 'itamae/mash'
5
5
 
6
6
  module Itamae
7
7
  class Node
@@ -10,7 +10,7 @@ module Itamae
10
10
  attr_reader :mash
11
11
 
12
12
  def initialize(hash, backend)
13
- @mash = Hashie::Mash.new(hash)
13
+ @mash = Itamae::Mash.new(hash)
14
14
  @backend = backend
15
15
  end
16
16
 
@@ -43,7 +43,7 @@ module Itamae
43
43
  private
44
44
 
45
45
  def _reverse_merge(other_hash)
46
- Hashie::Mash.new(other_hash).merge(@mash)
46
+ Itamae::Mash.new(other_hash).merge(@mash)
47
47
  end
48
48
 
49
49
  def method_missing(method, *args)
@@ -63,7 +63,7 @@ module Itamae
63
63
  def fetch_inventory_value(key)
64
64
  value = @backend.host_inventory[key]
65
65
  if value.is_a?(Hash)
66
- value = Hashie::Mash.new(value)
66
+ value = Itamae::Mash.new(value)
67
67
  end
68
68
 
69
69
  value
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  class Notification < Struct.new(:defined_in_resource, :action, :target_resource_desc, :timing)
5
3
  def self.create(*args)
data/lib/itamae/recipe.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  class Recipe
5
3
  NotFoundError = Class.new(StandardError)
@@ -55,7 +53,7 @@ module Itamae
55
53
 
56
54
  def load(vars = {})
57
55
  context = EvalContext.new(self, vars)
58
- context.instance_eval(File.read(path), path, 1)
56
+ InstanceEval.new(File.read(path), path, 1, context: context).call
59
57
  end
60
58
 
61
59
  def run
@@ -153,6 +151,25 @@ module Itamae
153
151
  end
154
152
  end
155
153
 
154
+ class InstanceEval
155
+ def initialize(src, path, lineno, context:)
156
+ # Using instance_eval + eval to allow top-level class/module definition without `::`.
157
+ # To pass args without introducing any local/instance variables, this code is also eval-ed.
158
+ @code = <<-RUBY
159
+ @context.instance_eval do
160
+ eval(#{src.dump}, nil, #{path.dump}, #{lineno})
161
+ end
162
+ RUBY
163
+ @context = context
164
+ end
165
+
166
+ # This method has no local variables to avoid spilling them to recipes.
167
+ def call
168
+ eval(@code)
169
+ end
170
+ end
171
+ private_constant :InstanceEval
172
+
156
173
  class RecipeFromDefinition < Recipe
157
174
  attr_accessor :definition
158
175
 
@@ -1,4 +1,3 @@
1
- require 'itamae'
2
1
  require 'shellwords'
3
2
  require 'hashie'
4
3
 
@@ -16,7 +15,7 @@ module Itamae
16
15
  def initialize(resource)
17
16
  @resource = resource
18
17
 
19
- @attributes = Hashie::Mash.new
18
+ @attributes = Itamae::Mash.new
20
19
  @notifications = []
21
20
  @subscriptions = []
22
21
  @verify_commands = []
@@ -196,6 +195,7 @@ module Itamae
196
195
  show_differences
197
196
 
198
197
  method_name = "action_#{action}"
198
+ Itamae.logger.debug "(in #{method_name})"
199
199
  if runner.dry_run?
200
200
  unless respond_to?(method_name)
201
201
  Itamae.logger.error "action #{action.inspect} is unavailable"
@@ -222,7 +222,7 @@ module Itamae
222
222
  end
223
223
 
224
224
  def clear_current_attributes
225
- @current_attributes = Hashie::Mash.new
225
+ @current_attributes = Itamae::Mash.new
226
226
  end
227
227
 
228
228
  def pre_action
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Directory < Base
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Execute < Base
@@ -1,16 +1,19 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class File < Base
6
4
  define_attribute :action, default: :create
7
5
  define_attribute :path, type: String, default_name: true
8
6
  define_attribute :content, type: String, default: nil
7
+ define_attribute :sensitive, default: false
9
8
  define_attribute :mode, type: String
10
9
  define_attribute :owner, type: String
11
10
  define_attribute :group, type: String
12
11
  define_attribute :block, type: Proc, default: proc {}
13
12
 
13
+ class << self
14
+ attr_accessor :sha256sum_available
15
+ end
16
+
14
17
  def pre_action
15
18
  current.exist = run_specinfra(:check_file_is_file, attributes.path)
16
19
 
@@ -29,6 +32,11 @@ module Itamae
29
32
  end
30
33
  end
31
34
 
35
+ if exists_and_not_modified?
36
+ attributes.modified = false
37
+ return
38
+ end
39
+
32
40
  send_tempfile
33
41
  compare_file
34
42
  end
@@ -135,10 +143,28 @@ module Itamae
135
143
  end
136
144
  end
137
145
 
146
+ def exists_and_not_modified?
147
+ return false unless current.exist && sha256sum_available?
148
+
149
+ current_digest = run_command(["sha256sum", attributes.path]).stdout.split(/\s/, 2).first
150
+ digest = if content_file
151
+ Digest::SHA256.file(content_file).hexdigest
152
+ else
153
+ Digest::SHA256.hexdigest(attributes.content.to_s)
154
+ end
155
+
156
+ current_digest == digest
157
+ end
158
+
138
159
  def show_content_diff
160
+ if attributes.sensitive
161
+ Itamae.logger.info("diff exists, but not displaying sensitive content")
162
+ return
163
+ end
164
+
139
165
  if attributes.modified
140
166
  Itamae.logger.info "diff:"
141
- diff = run_command(["diff", "-u", compare_to, @temppath], error: false)
167
+ diff = run_command(["diff", "-u", "--label=#{attributes.path} (BEFORE)", compare_to, "--label=#{attributes.path} (AFTER)", @temppath], error: false)
142
168
  diff.stdout.each_line do |line|
143
169
  color = if line.start_with?('+')
144
170
  :green
@@ -173,7 +199,12 @@ module Itamae
173
199
  src = if content_file
174
200
  content_file
175
201
  else
176
- f = Tempfile.open('itamae')
202
+ f =
203
+ if Gem.win_platform?
204
+ Tempfile.open('itamae', :mode=>IO::BINARY)
205
+ else
206
+ Tempfile.open('itamae')
207
+ end
177
208
  f.write(attributes.content)
178
209
  f.close
179
210
  f.path
@@ -183,12 +214,12 @@ module Itamae
183
214
 
184
215
  if backend.is_a?(Itamae::Backend::Docker)
185
216
  run_command(["mkdir", @temppath])
186
- backend.send_file(src, @temppath)
217
+ backend.send_file(src, @temppath, user: attributes.user)
187
218
  @temppath = ::File.join(@temppath, ::File.basename(src))
188
219
  else
189
220
  run_command(["touch", @temppath])
190
221
  run_specinfra(:change_file_mode, @temppath, '0600')
191
- backend.send_file(src, @temppath)
222
+ backend.send_file(src, @temppath, user: attributes.user)
192
223
  end
193
224
 
194
225
  run_specinfra(:change_file_mode, @temppath, '0600')
@@ -196,6 +227,12 @@ module Itamae
196
227
  f.unlink if f
197
228
  end
198
229
  end
230
+
231
+ def sha256sum_available?
232
+ return self.class.sha256sum_available unless self.class.sha256sum_available.nil?
233
+
234
+ self.class.sha256sum_available = run_command(["sha256sum", "--version"], error: false).exit_status == 0
235
+ end
199
236
  end
200
237
  end
201
238
  end
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class GemPackage < Base
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Git < Base
@@ -86,11 +84,11 @@ module Itamae
86
84
  end
87
85
 
88
86
  def get_revision(branch)
89
- result = run_command_in_repo("git rev-list #{shell_escape(branch)}", error: false)
90
- unless result.exit_status == 0
91
- fetch_origin!
92
- end
93
- run_command_in_repo("git rev-list #{shell_escape(branch)}").stdout.lines.first.strip
87
+ result = run_command_in_repo("git rev-parse #{shell_escape(branch)}", error: false)
88
+ return result.stdout.strip if result.exit_status == 0
89
+
90
+ fetch_origin!
91
+ run_command_in_repo("git rev-parse #{shell_escape(branch)}").stdout.strip
94
92
  end
95
93
 
96
94
  def fetch_origin!
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Group < Base
@@ -1,4 +1,3 @@
1
- require 'itamae'
2
1
  require 'uri'
3
2
  require 'net/https'
4
3
 
@@ -6,6 +5,9 @@ module Itamae
6
5
  module Resource
7
6
  class HttpRequest < File
8
7
  RedirectLimitExceeded = Class.new(StandardError)
8
+ HTTPClientError = Class.new(StandardError)
9
+ HTTPServerError = Class.new(StandardError)
10
+ HTTPUnknownError = Class.new(StandardError)
9
11
 
10
12
  alias_method :_action_create, :action_create
11
13
  undef_method :action_create, :action_delete, :action_edit
@@ -49,7 +51,10 @@ module Itamae
49
51
  response = http.method(attributes.action).call(uri.request_uri, attributes.message, attributes.headers)
50
52
  end
51
53
 
52
- if response.kind_of?(Net::HTTPRedirection)
54
+ case response
55
+ when Net::HTTPSuccess
56
+ break
57
+ when Net::HTTPRedirection
53
58
  if redirects_followed < attributes.redirect_limit
54
59
  uri = URI.parse(response["location"])
55
60
  redirects_followed += 1
@@ -57,8 +62,12 @@ module Itamae
57
62
  else
58
63
  raise RedirectLimitExceeded
59
64
  end
65
+ when Net::HTTPClientError
66
+ raise HTTPClientError
67
+ when Net::HTTPServerError
68
+ raise HTTPServerError
60
69
  else
61
- break
70
+ raise HTTPUnknownError
62
71
  end
63
72
  end
64
73
 
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Link < Base
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class LocalRubyBlock < Base
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Package < Base
@@ -26,6 +24,8 @@ module Itamae
26
24
  end
27
25
 
28
26
  def action_install(action_options)
27
+ return if !attributes.version && current.installed
28
+
29
29
  unless run_specinfra(:check_package_is_installed, attributes.name, attributes.version)
30
30
  run_specinfra(:install_package, attributes.name, attributes.version, attributes.options)
31
31
  updated!
@@ -33,7 +33,7 @@ module Itamae
33
33
  end
34
34
 
35
35
  def action_remove(action_options)
36
- if run_specinfra(:check_package_is_installed, attributes.name, nil)
36
+ if current.installed
37
37
  run_specinfra(:remove_package, attributes.name, attributes.options)
38
38
  updated!
39
39
  end
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class RemoteDirectory < Base
@@ -41,7 +39,7 @@ module Itamae
41
39
  super
42
40
 
43
41
  if current.exist
44
- diff = run_command(["diff", "-u", attributes.path, @temppath], error: false)
42
+ diff = run_command(["diff", "-u", "-r", attributes.path, @temppath], error: false)
45
43
  if diff.exit_status == 0
46
44
  # no change
47
45
  Itamae.logger.debug "directory content will not change"
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class RemoteFile < File
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class Service < Base
@@ -1,4 +1,3 @@
1
- require 'itamae'
2
1
  require 'erb'
3
2
  require 'tempfile'
4
3
 
@@ -38,9 +37,14 @@ module Itamae
38
37
 
39
38
  def render_file(src)
40
39
  template = ::File.read(src)
41
- ERB.new(template, nil, '-').tap do |erb|
42
- erb.filename = src
43
- end.result(binding)
40
+ erb =
41
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
42
+ ERB.new(template, trim_mode: '-')
43
+ else
44
+ ERB.new(template, nil, '-')
45
+ end
46
+ erb.filename = src
47
+ erb.result(binding)
44
48
  end
45
49
 
46
50
  def node
@@ -1,5 +1,3 @@
1
- require 'itamae'
2
-
3
1
  module Itamae
4
2
  module Resource
5
3
  class User < Base
@@ -1,4 +1,3 @@
1
- require 'itamae'
2
1
  require 'itamae/resource/base'
3
2
  require 'itamae/resource/file'
4
3
  require 'itamae/resource/package'
data/lib/itamae/runner.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'itamae'
2
1
  require 'json'
3
2
  require 'yaml'
4
3
 
@@ -31,7 +30,7 @@ module Itamae
31
30
  prepare_handler
32
31
 
33
32
  @node = create_node
34
- @tmpdir = "/tmp/itamae_tmp"
33
+ @tmpdir = options[:tmp_dir] || '/tmp/itamae_tmp'
35
34
  @children = RecipeChildren.new
36
35
  @diff = false
37
36
 
@@ -99,11 +98,11 @@ module Itamae
99
98
  unless @backend.run_command("which ohai", error: false).exit_status == 0
100
99
  # install Ohai
101
100
  Itamae.logger.info "Installing Chef package... (to use Ohai)"
102
- @backend.run_command("curl -L https://www.opscode.com/chef/install.sh | bash")
101
+ @backend.run_command("curl -L https://omnitruck.chef.io/install.sh | bash")
103
102
  end
104
103
 
105
104
  Itamae.logger.info "Loading node data via ohai..."
106
- hash.merge!(JSON.parse(@backend.run_command("ohai").stdout))
105
+ hash.merge!(JSON.parse(@backend.run_command("ohai 2>/dev/null").stdout))
107
106
  end
108
107
 
109
108
  if @options[:node_json]
@@ -1,3 +1,3 @@
1
1
  module Itamae
2
- VERSION = "1.10.0"
2
+ VERSION = "1.12.5"
3
3
  end
data/lib/itamae.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "itamae/version"
2
2
  require "itamae/runner"
3
- require "itamae/cli"
4
3
  require "itamae/recipe"
5
4
  require "itamae/resource"
6
5
  require "itamae/handler"
@@ -13,6 +12,7 @@ require "itamae/notification"
13
12
  require "itamae/definition"
14
13
  require "itamae/ext"
15
14
  require "itamae/generators"
15
+ require "itamae/mash"
16
16
 
17
17
  module Itamae
18
18
  # Your code goes here...
@@ -104,7 +104,7 @@ describe file('/tmp/http_request_headers.html') do
104
104
  its(:content) { should match(/"User-Agent":\s*"Itamae"/) }
105
105
  end
106
106
 
107
- describe file('/tmp/http_request_redirect.html') do
107
+ xdescribe file('/tmp/http_request_redirect.html') do
108
108
  it { should be_file }
109
109
  its(:content) { should match(/"from":\s*"itamae"/) }
110
110
  end
@@ -119,34 +119,6 @@ describe file('/tmp/subscribes') do
119
119
  its(:content) { should eq("2431") }
120
120
  end
121
121
 
122
- describe file('/tmp/cron_stopped') do
123
- it { should be_file }
124
- its(:content) do
125
- expect(subject.content.lines.size).to eq 1
126
- end
127
- end
128
-
129
- # FIXME: cron service is not running in docker...
130
- #
131
- # root@3450c6da6ea5:/# ps -C cron
132
- # PID TTY TIME CMD
133
- # root@3450c6da6ea5:/# service cron start
134
- # Rather than invoking init scripts through /etc/init.d, use the service(8)
135
- # utility, e.g. service cron start
136
- #
137
- # Since the script you are attempting to invoke has been converted to an
138
- # Upstart job, you may also use the start(8) utility, e.g. start cron
139
- # root@3450c6da6ea5:/# ps -C cron
140
- # PID TTY TIME CMD
141
- # root@3450c6da6ea5:/#
142
-
143
- # describe file('/tmp/cron_running') do
144
- # it { should be_file }
145
- # its(:content) do
146
- # expect(subject.content.lines.size).to eq 2
147
- # end
148
- # end
149
-
150
122
  describe file('/tmp-link') do
151
123
  it { should be_linked_to '/tmp' }
152
124
  its(:content) do
@@ -206,15 +178,19 @@ describe command('gem list') do
206
178
  end
207
179
 
208
180
  describe command('gem list') do
209
- its(:stdout) { should include('rake (11.1.0)') }
181
+ its(:stdout) { should match(/^rake \(.*11.1.0.*\)/) }
210
182
  end
211
183
 
212
184
  describe command('gem list') do
213
185
  its(:stdout) { should_not include('test-unit') }
214
186
  end
215
187
 
216
- describe command('ri Bundler') do
217
- its(:stderr) { should eq("Nothing known about Bundler\n") }
188
+ describe command('gem list') do
189
+ its(:stdout) { should include('ast (2.0.0)') }
190
+ end
191
+
192
+ describe command('ri AST') do
193
+ its(:stderr) { should eq("Nothing known about AST\n") }
218
194
  end
219
195
 
220
196
  describe file('/tmp/created_by_definition') do
@@ -357,3 +333,21 @@ describe file('/tmp/empty_file3') do
357
333
  it { should be_file }
358
334
  its(:content) { should eq "" }
359
335
  end
336
+
337
+ describe file('/tmp/toplevel_module') do
338
+ it { should exist }
339
+ it { should be_file }
340
+ its(:content) { should eq "helper" }
341
+ end
342
+
343
+ describe file('/tmp/local_variables') do
344
+ it { should exist }
345
+ it { should be_file }
346
+ its(:content) { should eq "[]" }
347
+ end
348
+
349
+ describe file('/tmp/instance_variables') do
350
+ it { should exist }
351
+ it { should be_file }
352
+ its(:content) { should eq "[:@recipe]" } # backward compatibility
353
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe file('/tmp/cron_stopped') do
4
+ it { should be_file }
5
+ its(:content) do
6
+ expect(subject.content.lines.size).to eq 1
7
+ end
8
+ end
9
+
10
+ # FIXME: cron service is not running in docker...
11
+ #
12
+ # root@3450c6da6ea5:/# ps -C cron
13
+ # PID TTY TIME CMD
14
+ # root@3450c6da6ea5:/# service cron start
15
+ # Rather than invoking init scripts through /etc/init.d, use the service(8)
16
+ # utility, e.g. service cron start
17
+ #
18
+ # Since the script you are attempting to invoke has been converted to an
19
+ # Upstart job, you may also use the start(8) utility, e.g. start cron
20
+ # root@3450c6da6ea5:/# ps -C cron
21
+ # PID TTY TIME CMD
22
+ # root@3450c6da6ea5:/#
23
+
24
+ # describe file('/tmp/cron_running') do
25
+ # it { should be_file }
26
+ # its(:content) do
27
+ # expect(subject.content.lines.size).to eq 2
28
+ # end
29
+ # end
@@ -0,0 +1,6 @@
1
+ describe file('/tmp/file_as_ordinary_user') do
2
+ it { should be_file }
3
+ it { should be_owned_by "itamae" }
4
+ it { should be_grouped_into "itamae" }
5
+ end
6
+