r10k 3.10.0 → 3.11.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -10
  3. data/CHANGELOG.mkd +9 -0
  4. data/README.mkd +6 -0
  5. data/doc/dynamic-environments/configuration.mkd +14 -0
  6. data/doc/puppetfile.mkd +15 -1
  7. data/integration/Rakefile +2 -0
  8. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +15 -13
  9. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  10. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  11. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  12. data/lib/r10k/action/deploy/environment.rb +6 -1
  13. data/lib/r10k/action/deploy/module.rb +31 -5
  14. data/lib/r10k/action/runner.rb +34 -4
  15. data/lib/r10k/cli/deploy.rb +4 -0
  16. data/lib/r10k/git.rb +3 -0
  17. data/lib/r10k/git/rugged/credentials.rb +77 -0
  18. data/lib/r10k/git/stateful_repository.rb +1 -0
  19. data/lib/r10k/initializers.rb +3 -0
  20. data/lib/r10k/module/base.rb +37 -0
  21. data/lib/r10k/module/forge.rb +1 -0
  22. data/lib/r10k/module/git.rb +1 -0
  23. data/lib/r10k/module/svn.rb +1 -0
  24. data/lib/r10k/module_loader/puppetfile.rb +15 -4
  25. data/lib/r10k/puppetfile.rb +10 -11
  26. data/lib/r10k/settings.rb +44 -2
  27. data/lib/r10k/settings/definition.rb +1 -1
  28. data/lib/r10k/version.rb +1 -1
  29. data/locales/r10k.pot +106 -38
  30. data/r10k.gemspec +2 -0
  31. data/spec/unit/action/deploy/environment_spec.rb +16 -0
  32. data/spec/unit/action/deploy/module_spec.rb +178 -0
  33. data/spec/unit/action/runner_spec.rb +80 -0
  34. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  35. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  36. data/spec/unit/module/base_spec.rb +46 -0
  37. data/spec/unit/module/forge_spec.rb +19 -0
  38. data/spec/unit/module/git_spec.rb +17 -0
  39. data/spec/unit/module/svn_spec.rb +18 -0
  40. data/spec/unit/module_loader/puppetfile_spec.rb +16 -3
  41. data/spec/unit/module_spec.rb +12 -1
  42. data/spec/unit/puppetfile_spec.rb +31 -1
  43. data/spec/unit/settings_spec.rb +18 -0
  44. metadata +16 -2
data/r10k.gemspec CHANGED
@@ -36,6 +36,8 @@ Gem::Specification.new do |s|
36
36
  s.add_dependency 'fast_gettext', '~> 1.1.0'
37
37
  s.add_dependency 'gettext', ['>= 3.0.2', '< 3.3.0']
38
38
 
39
+ s.add_dependency 'jwt', '~> 2.2.3'
40
+
39
41
  s.add_development_dependency 'rspec', '~> 3.1'
40
42
 
41
43
  s.add_development_dependency 'rake'
@@ -47,6 +47,22 @@ describe R10K::Action::Deploy::Environment do
47
47
  described_class.new({ 'oauth-token': '/nonexistent' }, [], {})
48
48
  end
49
49
 
50
+ it 'can accept an app id option' do
51
+ described_class.new({ 'github-app-id': '/nonexistent' }, [], {})
52
+ end
53
+
54
+ it 'can accept a ttl option' do
55
+ described_class.new({ 'github-app-ttl': '/nonexistent' }, [], {})
56
+ end
57
+
58
+ it 'can accept a ssl private key option' do
59
+ described_class.new({ 'github-app-key': '/nonexistent' }, [], {})
60
+ end
61
+
62
+ it 'can accept a exclude-spec option' do
63
+ described_class.new({ :'exclude-spec' => true }, [], {})
64
+ end
65
+
50
66
  describe "initializing errors" do
51
67
  let (:settings) { { deploy: { purge_levels: [:environment],
52
68
  purge_whitelist: ['coolfile', 'coolfile2'],
@@ -41,6 +41,22 @@ describe R10K::Action::Deploy::Module do
41
41
  it 'can accept a token option' do
42
42
  described_class.new({ 'oauth-token': '/nonexistent' }, [], {})
43
43
  end
44
+
45
+ it 'can accept an app id option' do
46
+ described_class.new({ 'github-app-id': '/nonexistent' }, [], {})
47
+ end
48
+
49
+ it 'can accept a ttl option' do
50
+ described_class.new({ 'github-app-ttl': '/nonexistent' }, [], {})
51
+ end
52
+
53
+ it 'can accept a ssl private key option' do
54
+ described_class.new({ 'github-app-key': '/nonexistent' }, [], {})
55
+ end
56
+
57
+ it 'can accept a exclude-spec option' do
58
+ described_class.new({ :'exclude-spec' => true }, [], {})
59
+ end
44
60
  end
45
61
 
46
62
  describe "with no-force" do
@@ -177,6 +193,33 @@ describe R10K::Action::Deploy::Module do
177
193
  end
178
194
  end
179
195
 
196
+ describe 'with github-app-id' do
197
+
198
+ subject { described_class.new({ config: '/some/nonexistent/path', 'github-app-id': '/nonexistent' }, [], {}) }
199
+
200
+ it 'sets github-app-id' do
201
+ expect(subject.instance_variable_get(:@github_app_id)).to eq('/nonexistent')
202
+ end
203
+ end
204
+
205
+ describe 'with github-app-key' do
206
+
207
+ subject { described_class.new({ config: '/some/nonexistent/path', 'github-app-key': '/nonexistent' }, [], {}) }
208
+
209
+ it 'sets github-app-key' do
210
+ expect(subject.instance_variable_get(:@github_app_key)).to eq('/nonexistent')
211
+ end
212
+ end
213
+
214
+ describe 'with github-app-ttl' do
215
+
216
+ subject { described_class.new({ config: '/some/nonexistent/path', 'github-app-ttl': '/nonexistent' }, [], {}) }
217
+
218
+ it 'sets github-app-ttl' do
219
+ expect(subject.instance_variable_get(:@github_app_ttl)).to eq('/nonexistent')
220
+ end
221
+ end
222
+
180
223
  describe 'with modules' do
181
224
 
182
225
  subject { described_class.new({ config: '/some/nonexistent/path' }, ['mod1', 'mod2'], {}) }
@@ -306,5 +349,140 @@ describe R10K::Action::Deploy::Module do
306
349
  subject.call
307
350
  end
308
351
  end
352
+
353
+
354
+ describe "postrun" do
355
+ let(:mock_config) do
356
+ R10K::Deployment::MockConfig.new(
357
+ :sources => {
358
+ :control => {
359
+ :type => :mock,
360
+ :basedir => '/some/nonexistent/path/control',
361
+ :environments => %w[first second third],
362
+ }
363
+ }
364
+ )
365
+ end
366
+
367
+ context "basic postrun hook" do
368
+ let(:settings) { { postrun: ["/path/to/executable", "arg1", "arg2"] } }
369
+ let(:deployment) { R10K::Deployment.new(mock_config.merge(settings)) }
370
+
371
+ before do
372
+ expect(R10K::Deployment).to receive(:new).and_return(deployment)
373
+ end
374
+
375
+ subject do
376
+ described_class.new({config: "/some/nonexistent/path" },
377
+ ['mod1'], settings)
378
+ end
379
+
380
+ it "is passed to Subprocess" do
381
+ mock_subprocess = double
382
+ allow(mock_subprocess).to receive(:logger=)
383
+ expect(mock_subprocess).to receive(:execute)
384
+
385
+ expect(R10K::Util::Subprocess).to receive(:new).
386
+ with(["/path/to/executable", "arg1", "arg2"]).
387
+ and_return(mock_subprocess)
388
+
389
+ expect(subject).to receive(:visit_environment).and_wrap_original do |original, environment, &block|
390
+ modified = subject.instance_variable_get(:@modified_envs) << environment
391
+ subject.instance_variable_set(:modified_envs, modified)
392
+ end.exactly(3).times
393
+
394
+ subject.call
395
+ end
396
+ end
397
+
398
+ context "supports environments" do
399
+ context "with one environment" do
400
+ let(:settings) { { postrun: ["/generate/types/wrapper", "$modifiedenvs"] } }
401
+ let(:deployment) { R10K::Deployment.new(mock_config.merge(settings)) }
402
+
403
+ before do
404
+ expect(R10K::Deployment).to receive(:new).and_return(deployment)
405
+ end
406
+
407
+ subject do
408
+ described_class.new({ config: '/some/nonexistent/path',
409
+ environment: 'first' },
410
+ ['mod1'], settings)
411
+ end
412
+
413
+ it "properly substitutes the environment" do
414
+ mock_subprocess = double
415
+ allow(mock_subprocess).to receive(:logger=)
416
+ expect(mock_subprocess).to receive(:execute)
417
+
418
+ mock_mod = double('mock_mod', name: 'mod1')
419
+
420
+ expect(R10K::Util::Subprocess).to receive(:new).
421
+ with(["/generate/types/wrapper", "first"]).
422
+ and_return(mock_subprocess)
423
+
424
+ expect(subject).to receive(:visit_environment).and_wrap_original do |original, environment, &block|
425
+ if environment.name == 'first'
426
+ expect(environment).to receive(:deploy).and_return(true)
427
+ expect(environment).to receive(:modules).and_return([mock_mod])
428
+ end
429
+ original.call(environment, &block)
430
+ end.exactly(3).times
431
+
432
+ subject.call
433
+ end
434
+ end
435
+
436
+ context "with all environments" do
437
+ let(:settings) { { postrun: ["/generate/types/wrapper", "$modifiedenvs"] } }
438
+ let(:deployment) { R10K::Deployment.new(mock_config.merge(settings)) }
439
+
440
+ before do
441
+ expect(R10K::Deployment).to receive(:new).and_return(deployment)
442
+ end
443
+
444
+ subject do
445
+ described_class.new({ config: '/some/nonexistent/path' },
446
+ ['mod1'], settings)
447
+ end
448
+
449
+ it "properly substitutes the environment where modules were deployed" do
450
+ mock_subprocess = double
451
+ allow(mock_subprocess).to receive(:logger=)
452
+ expect(mock_subprocess).to receive(:execute)
453
+
454
+ expect(R10K::Util::Subprocess).to receive(:new).
455
+ with(["/generate/types/wrapper", "first third"]).
456
+ and_return(mock_subprocess)
457
+
458
+ mock_mod = double('mock_mod', name: 'mod1')
459
+
460
+ expect(subject).to receive(:visit_environment).and_wrap_original do |original, environment, &block|
461
+ expect(environment).to receive(:deploy).and_return(true)
462
+ if ['first', 'third'].include?(environment.name)
463
+ expect(environment).to receive(:modules).and_return([mock_mod])
464
+ end
465
+ original.call(environment, &block)
466
+ end.exactly(3).times
467
+
468
+ subject.call
469
+ end
470
+
471
+ it "does not execute the command if no envs had the module" do
472
+ expect(R10K::Util::Subprocess).not_to receive(:new)
473
+
474
+ mock_mod2 = double('mock_mod', name: 'mod2')
475
+ expect(subject).to receive(:visit_environment).and_wrap_original do |original, environment, &block|
476
+ expect(environment).to receive(:deploy).and_return(true)
477
+ # Envs have a different module than the one we asked to deploy
478
+ expect(environment).to receive(:modules).and_return([mock_mod2])
479
+ original.call(environment, &block)
480
+ end.exactly(3).times
481
+
482
+ subject.call
483
+ end
484
+ end
485
+ end
486
+ end
309
487
  end
310
488
 
@@ -171,6 +171,86 @@ describe R10K::Action::Runner do
171
171
  end
172
172
  end
173
173
 
174
+ describe "configuring github app credentials" do
175
+ it 'errors if app id is passed without ssl key' do
176
+ runner = described_class.new(
177
+ { 'github-app-id': '/nonexistent', },
178
+ %w[args yes],
179
+ action_class
180
+ )
181
+ expect{ runner.call }.to raise_error(R10K::Error, /Must specify both id and SSL private key/)
182
+ end
183
+
184
+ it 'errors if ssl key is passed without app id' do
185
+ runner = described_class.new(
186
+ { 'github-app-key': '/nonexistent', },
187
+ %w[args yes],
188
+ action_class
189
+ )
190
+ expect{ runner.call }.to raise_error(R10K::Error, /Must specify both id and SSL private key/)
191
+ end
192
+
193
+ it 'errors if both app id and token paths are passed' do
194
+ runner = described_class.new(
195
+ { 'github-app-id': '/nonexistent', 'oauth-token': '/also/fake' },
196
+ %w[args yes],
197
+ action_class
198
+ )
199
+ expect{ runner.call }.to raise_error(R10K::Error, /Cannot specify both/)
200
+ end
201
+
202
+ it 'errors if both ssl key and token paths are passed' do
203
+ runner = described_class.new(
204
+ { 'github-app-key': '/nonexistent', 'oauth-token': '/also/fake' },
205
+ %w[args yes],
206
+ action_class
207
+ )
208
+ expect{ runner.call }.to raise_error(R10K::Error, /Cannot specify both/)
209
+ end
210
+
211
+ it 'errors if both ssl key and ssh key paths are passed' do
212
+ runner = described_class.new(
213
+ { 'github-app-key': '/nonexistent', 'private-key': '/also/fake' },
214
+ %w[args yes],
215
+ action_class
216
+ )
217
+ expect{ runner.call }.to raise_error(R10K::Error, /Cannot specify both/)
218
+ end
219
+
220
+ it 'errors if both app id and ssh key are passed' do
221
+ runner = described_class.new(
222
+ { 'github-app-id': '/nonexistent', 'private-key': '/also/fake' },
223
+ %w[args yes],
224
+ action_class
225
+ )
226
+ expect{ runner.call }.to raise_error(R10K::Error, /Cannot specify both/)
227
+ end
228
+
229
+ it 'saves the parameters in settings hash' do
230
+ runner = described_class.new(
231
+ { 'github-app-id': '123456', 'github-app-key': '/my/ssl/key', 'github-app-ttl': '600' },
232
+ %w[args yes],
233
+ action_class
234
+ )
235
+ runner.call
236
+ expect(runner.instance.settings[:git][:github_app_id]).to eq('123456')
237
+ expect(runner.instance.settings[:git][:github_app_key]).to eq('/my/ssl/key')
238
+ expect(runner.instance.settings[:git][:github_app_ttl]).to eq('600')
239
+ end
240
+
241
+ it 'saves the parameters in settings hash without ttl and uses its default value' do
242
+ runner = described_class.new(
243
+ { 'github-app-id': '123456', 'github-app-key': '/my/ssl/key', },
244
+ %w[args yes],
245
+ action_class
246
+ )
247
+ runner.call
248
+ expect(runner.instance.settings[:git][:github_app_id]).to eq('123456')
249
+ expect(runner.instance.settings[:git][:github_app_key]).to eq('/my/ssl/key')
250
+ expect(runner.instance.settings[:git][:github_app_ttl]).to eq('120')
251
+ end
252
+ end
253
+
174
254
  describe "configuring git credentials" do
175
255
  it 'errors if both token and key paths are passed' do
176
256
  runner = described_class.new({ 'oauth-token': '/nonexistent',
@@ -79,6 +79,35 @@ describe R10K::Git::Rugged::Credentials, :unless => R10K::Util::Platform.jruby?
79
79
  end
80
80
  end
81
81
 
82
+ describe "generating github app tokens" do
83
+ it 'errors if app id has invalid characters' do
84
+ expect { subject.github_app_token("123A567890", "fake", "300")
85
+ }.to raise_error(R10K::Git::GitError, /App id contains invalid characters/)
86
+ end
87
+ it 'errors if app ttl has invalid characters' do
88
+ expect { subject.github_app_token("123456", "fake", "abc")
89
+ }.to raise_error(R10K::Git::GitError, /Github App token ttl contains/)
90
+ end
91
+ it 'errors if private file does not exist' do
92
+ R10K::Git.settings[:github_app_key] = "/missing/token/file"
93
+ expect(File).to receive(:readable?).with(R10K::Git.settings[:github_app_key]).and_return false
94
+ expect {
95
+ subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
96
+ }.to raise_error(R10K::Git::GitError, /App key is missing or unreadable/)
97
+ end
98
+ it 'errors if file is not a valid SSL key' do
99
+ token_file = Tempfile.new('token')
100
+ token_file.write('my_token')
101
+ token_file.close
102
+ R10K::Git.settings[:github_app_key] = token_file.path
103
+ expect(File).to receive(:readable?).with(token_file.path).and_return true
104
+ expect {
105
+ subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
106
+ }.to raise_error(R10K::Git::GitError, /App key is not a valid SSL key/)
107
+ token_file.unlink
108
+ end
109
+ end
110
+
82
111
  describe "generating token credentials" do
83
112
  it 'errors if token file does not exist' do
84
113
  R10K::Git.settings[:oauth_token] = "/missing/token/file"
@@ -19,6 +19,11 @@ describe R10K::Git::StatefulRepository do
19
19
  expect(subject.sync_cache?(ref)).to eq true
20
20
  end
21
21
 
22
+ it "is true if the ref is HEAD" do
23
+ expect(cache).to receive(:exist?).and_return true
24
+ expect(subject.sync_cache?('HEAD')).to eq true
25
+ end
26
+
22
27
  it "is true if the ref is unresolvable" do
23
28
  expect(cache).to receive(:exist?).and_return true
24
29
  expect(cache).to receive(:ref_type).with('0.9.x').and_return(:unknown)
@@ -28,6 +28,52 @@ describe R10K::Module::Base do
28
28
  end
29
29
  end
30
30
 
31
+ describe 'deleting the spec dir' do
32
+ let(:module_org) { "coolorg" }
33
+ let(:module_name) { "coolmod" }
34
+ let(:title) { "#{module_org}-#{module_name}" }
35
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
36
+ let(:spec_path) { dirname + module_name + 'spec' }
37
+
38
+ before(:each) do
39
+ logger = double("logger")
40
+ allow_any_instance_of(described_class).to receive(:logger).and_return(logger)
41
+ allow(logger).to receive(:debug2).with(any_args)
42
+ allow(logger).to receive(:info).with(any_args)
43
+ end
44
+
45
+ it 'does not remove the spec directory by default' do
46
+ FileUtils.mkdir_p(spec_path)
47
+ m = described_class.new(title, dirname, {})
48
+ m.maybe_delete_spec_dir
49
+ expect(Dir.exist?(spec_path)).to eq true
50
+ end
51
+
52
+ it 'detects a symlink and deletes the target' do
53
+ Dir.mkdir(dirname + module_name)
54
+ target_dir = Dir.mktmpdir
55
+ FileUtils.ln_s(target_dir, spec_path)
56
+ m = described_class.new(title, dirname, {exclude_spec: true})
57
+ m.maybe_delete_spec_dir
58
+ expect(Dir.exist?(target_dir)).to eq false
59
+ end
60
+
61
+ it 'removes the spec directory if exclude_spec is set' do
62
+ FileUtils.mkdir_p(spec_path)
63
+ m = described_class.new(title, dirname, {exclude_spec: true})
64
+ m.maybe_delete_spec_dir
65
+ expect(Dir.exist?(spec_path)).to eq false
66
+ end
67
+
68
+ it 'does not remove the spec directory if spec_deletable is false' do
69
+ FileUtils.mkdir_p(spec_path)
70
+ m = described_class.new(title, dirname, {})
71
+ m.spec_deletable = false
72
+ m.maybe_delete_spec_dir
73
+ expect(Dir.exist?(spec_path)).to eq true
74
+ end
75
+ end
76
+
31
77
  describe "path variables" do
32
78
  it "uses the module name as the name" do
33
79
  m = described_class.new('eight_hundred', '/moduledir', [])
@@ -78,6 +78,7 @@ describe R10K::Module::Forge do
78
78
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
79
79
 
80
80
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
81
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
81
82
  expect(logger_dbl).to receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
82
83
 
83
84
  subject.sync
@@ -90,6 +91,7 @@ describe R10K::Module::Forge do
90
91
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
91
92
 
92
93
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
94
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
93
95
  expect(logger_dbl).to_not receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
94
96
 
95
97
  subject.sync
@@ -165,6 +167,23 @@ describe R10K::Module::Forge do
165
167
  describe "#sync" do
166
168
  subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }
167
169
 
170
+ context "syncing the repo" do
171
+ let(:module_org) { "coolorg" }
172
+ let(:module_name) { "coolmod" }
173
+ let(:title) { "#{module_org}-#{module_name}" }
174
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
175
+ let(:spec_path) { dirname + module_name + 'spec' }
176
+ subject { described_class.new(title, dirname, {}) }
177
+
178
+ it 'defaults to keeping the spec dir' do
179
+ FileUtils.mkdir_p(spec_path)
180
+ expect(subject).to receive(:status).and_return(:absent)
181
+ expect(subject).to receive(:install)
182
+ subject.sync
183
+ expect(Dir.exist?(spec_path)).to eq true
184
+ end
185
+ end
186
+
168
187
  it 'does nothing when the module is in sync' do
169
188
  allow(subject).to receive(:status).and_return :insync
170
189