itamae 1.10.0 → 1.12.5

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 (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
+