r10k 3.10.0 → 3.11.0

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