r10k 3.9.1 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec_tests.yml +1 -1
  3. data/.travis.yml +0 -10
  4. data/CHANGELOG.mkd +28 -0
  5. data/README.mkd +6 -0
  6. data/doc/dynamic-environments/configuration.mkd +21 -0
  7. data/doc/puppetfile.mkd +15 -1
  8. data/integration/Rakefile +3 -1
  9. data/integration/tests/user_scenario/basic_workflow/negative/neg_specify_deleted_forge_module.rb +3 -9
  10. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +21 -25
  11. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  12. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  13. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  14. data/lib/r10k/action/base.rb +6 -3
  15. data/lib/r10k/action/deploy/display.rb +6 -3
  16. data/lib/r10k/action/deploy/environment.rb +15 -4
  17. data/lib/r10k/action/deploy/module.rb +37 -8
  18. data/lib/r10k/action/runner.rb +45 -10
  19. data/lib/r10k/cli/deploy.rb +4 -0
  20. data/lib/r10k/git.rb +3 -0
  21. data/lib/r10k/git/cache.rb +1 -1
  22. data/lib/r10k/git/rugged/credentials.rb +77 -0
  23. data/lib/r10k/git/stateful_repository.rb +1 -0
  24. data/lib/r10k/initializers.rb +10 -0
  25. data/lib/r10k/module/base.rb +37 -0
  26. data/lib/r10k/module/forge.rb +7 -2
  27. data/lib/r10k/module/git.rb +2 -1
  28. data/lib/r10k/module/svn.rb +2 -1
  29. data/lib/r10k/module_loader/puppetfile.rb +206 -0
  30. data/lib/r10k/module_loader/puppetfile/dsl.rb +37 -0
  31. data/lib/r10k/puppetfile.rb +83 -160
  32. data/lib/r10k/settings.rb +47 -2
  33. data/lib/r10k/settings/definition.rb +1 -1
  34. data/lib/r10k/source/base.rb +10 -0
  35. data/lib/r10k/source/git.rb +5 -0
  36. data/lib/r10k/source/svn.rb +4 -0
  37. data/lib/r10k/util/purgeable.rb +70 -8
  38. data/lib/r10k/version.rb +1 -1
  39. data/locales/r10k.pot +129 -57
  40. data/r10k.gemspec +2 -0
  41. data/spec/fixtures/unit/action/r10k_forge_auth.yaml +4 -0
  42. data/spec/fixtures/unit/action/r10k_forge_auth_no_url.yaml +3 -0
  43. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_subdir_2/ignored_1 +0 -0
  44. data/spec/fixtures/unit/util/purgeable/managed_two/.hidden/unmanaged_3 +0 -0
  45. data/spec/unit/action/deploy/environment_spec.rb +25 -0
  46. data/spec/unit/action/deploy/module_spec.rb +216 -14
  47. data/spec/unit/action/runner_spec.rb +129 -25
  48. data/spec/unit/git/cache_spec.rb +14 -0
  49. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  50. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  51. data/spec/unit/module/base_spec.rb +46 -0
  52. data/spec/unit/module/forge_spec.rb +27 -1
  53. data/spec/unit/module/git_spec.rb +17 -8
  54. data/spec/unit/module/svn_spec.rb +18 -0
  55. data/spec/unit/module_loader/puppetfile_spec.rb +343 -0
  56. data/spec/unit/module_spec.rb +28 -0
  57. data/spec/unit/puppetfile_spec.rb +127 -191
  58. data/spec/unit/settings_spec.rb +24 -2
  59. data/spec/unit/util/purgeable_spec.rb +38 -6
  60. metadata +23 -2
@@ -62,4 +62,18 @@ describe R10K::Git::Cache do
62
62
  subject.cached?
63
63
  end
64
64
  end
65
+
66
+ describe "dirname sanitization" do
67
+ it 'sanitizes cache directory name' do
68
+ expect(subject.sanitized_dirname).to eq('git---some-git-remote')
69
+ end
70
+
71
+ context 'with username and password' do
72
+ subject { subclass.new('https://"user:pa$$w0rd:@some/git/remote') }
73
+
74
+ it 'sanitizes cache directory name' do
75
+ expect(subject.sanitized_dirname).to eq('https---some-git-remote')
76
+ end
77
+ end
78
+ end
65
79
  end
@@ -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
@@ -104,9 +106,16 @@ describe R10K::Module::Forge do
104
106
 
105
107
  it "uses the latest version from the forge when the version is :latest" do
106
108
  subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: :latest })
107
- expect(subject.v3_module).to receive_message_chain(:current_release, :version).and_return('8.8.8')
109
+ release = double("Module Release", version: '8.8.8')
110
+ expect(subject.v3_module).to receive(:current_release).and_return(release).twice
108
111
  expect(subject.expected_version).to eq '8.8.8'
109
112
  end
113
+
114
+ it "throws when there are no available versions" do
115
+ subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: :latest })
116
+ expect(subject.v3_module).to receive(:current_release).and_return(nil)
117
+ expect { subject.expected_version }.to raise_error(PuppetForge::ReleaseNotFound)
118
+ end
110
119
  end
111
120
 
112
121
  describe "determining the status" do
@@ -158,6 +167,23 @@ describe R10K::Module::Forge do
158
167
  describe "#sync" do
159
168
  subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }
160
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
+
161
187
  it 'does nothing when the module is in sync' do
162
188
  allow(subject).to receive(:status).and_return :insync
163
189
 
@@ -89,6 +89,23 @@ describe R10K::Module::Git do
89
89
  end
90
90
  end
91
91
 
92
+ describe 'syncing the repo' do
93
+ let(:module_org) { "coolorg" }
94
+ let(:module_name) { "coolmod" }
95
+ let(:title) { "#{module_org}-#{module_name}" }
96
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
97
+ let(:spec_path) { dirname + module_name + 'spec' }
98
+ subject { described_class.new(title, dirname, {}) }
99
+
100
+ it 'defaults to keeping the spec dir' do
101
+ FileUtils.mkdir_p(spec_path)
102
+ allow(mock_repo).to receive(:resolve).with('master').and_return('abc123')
103
+ allow(mock_repo).to receive(:sync)
104
+ subject.sync
105
+ expect(Dir.exist?(spec_path)).to eq true
106
+ end
107
+ end
108
+
92
109
  describe "determining the status" do
93
110
  subject do
94
111
  described_class.new(
@@ -119,14 +136,6 @@ describe R10K::Module::Git do
119
136
  allow(mock_repo).to receive(:head).and_return('abc123')
120
137
  end
121
138
 
122
- context "when option is unrecognized" do
123
- let(:opts) { { unrecognized: true } }
124
-
125
- it "raises an error" do
126
- expect { test_module(opts) }.to raise_error(ArgumentError, /cannot handle option 'unrecognized'/)
127
- end
128
- end
129
-
130
139
  describe "desired ref" do
131
140
  context "when no desired ref is given" do
132
141
  it "defaults to master" do
@@ -119,6 +119,24 @@ describe R10K::Module::SVN do
119
119
  end
120
120
  end
121
121
 
122
+ describe 'the default spec dir' do
123
+ let(:module_org) { "coolorg" }
124
+ let(:module_name) { "coolmod" }
125
+ let(:title) { "#{module_org}-#{module_name}" }
126
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
127
+ let(:spec_path) { dirname + module_name + 'spec' }
128
+ subject { described_class.new(title, dirname, {}) }
129
+
130
+ it 'is kept by default' do
131
+
132
+ FileUtils.mkdir_p(spec_path)
133
+ expect(subject).to receive(:status).and_return(:absent)
134
+ expect(subject).to receive(:install).and_return(nil)
135
+ subject.sync
136
+ expect(Dir.exist?(spec_path)).to eq true
137
+ end
138
+ end
139
+
122
140
  describe "synchronizing" do
123
141
 
124
142
  subject { described_class.new('foo', '/moduledir', :svn => 'https://github.com/adrienthebo/r10k-fixture-repo', :rev => 123) }
@@ -0,0 +1,343 @@
1
+ require 'spec_helper'
2
+ require 'r10k/module_loader/puppetfile'
3
+
4
+ describe R10K::ModuleLoader::Puppetfile do
5
+ describe 'initial parameters' do
6
+ describe 'honor' do
7
+ let(:options) do
8
+ {
9
+ basedir: '/test/basedir/env',
10
+ forge: 'localforge.internal.corp',
11
+ overrides: { modules: { deploy_modules: true } },
12
+ environment: R10K::Environment::Git.new('env',
13
+ '/test/basedir/',
14
+ 'env',
15
+ { remote: 'git://foo/remote',
16
+ ref: 'env' })
17
+ }
18
+ end
19
+
20
+ subject { R10K::ModuleLoader::Puppetfile.new(**options) }
21
+
22
+ describe 'the moduledir' do
23
+ it 'respects absolute paths' do
24
+ absolute_options = options.merge({moduledir: '/opt/puppetlabs/special/modules'})
25
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**absolute_options)
26
+ expect(puppetfile.instance_variable_get(:@moduledir)).to eq('/opt/puppetlabs/special/modules')
27
+ end
28
+
29
+ it 'roots the moduledir in the basepath if a relative path is specified' do
30
+ relative_options = options.merge({moduledir: 'my/special/modules'})
31
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**relative_options)
32
+ expect(puppetfile.instance_variable_get(:@moduledir)).to eq('/test/basedir/env/my/special/modules')
33
+ end
34
+ end
35
+
36
+ describe 'the Puppetfile' do
37
+ it 'respects absolute paths' do
38
+ absolute_options = options.merge({puppetfile: '/opt/puppetlabs/special/Puppetfile'})
39
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**absolute_options)
40
+ expect(puppetfile.instance_variable_get(:@puppetfile_path)).to eq('/opt/puppetlabs/special/Puppetfile')
41
+ end
42
+
43
+ it 'roots the Puppetfile in the basepath if a relative path is specified' do
44
+ relative_options = options.merge({puppetfile: 'Puppetfile.global'})
45
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**relative_options)
46
+ expect(puppetfile.instance_variable_get(:@puppetfile_path)).to eq('/test/basedir/env/Puppetfile.global')
47
+ end
48
+ end
49
+
50
+ it 'the forge' do
51
+ expect(subject.instance_variable_get(:@forge)).to eq('localforge.internal.corp')
52
+ end
53
+
54
+ it 'the overrides' do
55
+ expect(subject.instance_variable_get(:@overrides)).to eq({ modules: { deploy_modules: true }})
56
+ end
57
+
58
+ it 'the environment' do
59
+ expect(subject.instance_variable_get(:@environment).name).to eq('env')
60
+ end
61
+ end
62
+
63
+ describe 'sane defaults' do
64
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir') }
65
+
66
+ it 'has a moduledir rooted in the basedir' do
67
+ expect(subject.instance_variable_get(:@moduledir)).to eq('/test/basedir/modules')
68
+ end
69
+
70
+ it 'has a Puppetfile rooted in the basedir' do
71
+ expect(subject.instance_variable_get(:@puppetfile_path)).to eq('/test/basedir/Puppetfile')
72
+ end
73
+
74
+ it 'uses the public forge' do
75
+ expect(subject.instance_variable_get(:@forge)).to eq('forgeapi.puppetlabs.com')
76
+ end
77
+
78
+ it 'creates an empty overrides' do
79
+ expect(subject.instance_variable_get(:@overrides)).to eq({})
80
+ end
81
+
82
+ it 'does not require an environment' do
83
+ expect(subject.instance_variable_get(:@environment)).to eq(nil)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe 'adding modules' do
89
+ let(:basedir) { '/test/basedir' }
90
+
91
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: basedir) }
92
+
93
+ it 'should transform Forge modules with a string arg to have a version key' do
94
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '1.2.3'), anything).and_call_original
95
+
96
+ expect { subject.add_module('puppet/test_module', '1.2.3') }.to change { subject.modules }
97
+ expect(subject.modules.collect(&:name)).to include('test_module')
98
+ end
99
+
100
+ it 'should not accept Forge modules with a version comparison' do
101
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '< 1.2.0'), anything).and_call_original
102
+
103
+ expect {
104
+ subject.add_module('puppet/test_module', '< 1.2.0')
105
+ }.to raise_error(RuntimeError, /module puppet\/test_module.*doesn't have an implementation/i)
106
+
107
+ expect(subject.modules.collect(&:name)).not_to include('test_module')
108
+ end
109
+
110
+ it 'should set :spec_deletable to true for modules in the basedir' do
111
+ module_opts = { git: 'git@example.com:puppet/test_module.git' }
112
+ subject.add_module('puppet/test_module', module_opts)
113
+ expect(subject.modules[0].spec_deletable).to be true
114
+ end
115
+
116
+ it 'should set :spec_deletable to false for modules outside the basedir' do
117
+ module_opts = { git: 'git@example.com:puppet/test_module.git', install_path: 'some/path' }
118
+ subject.add_module('puppet/test_module', module_opts)
119
+ expect(subject.modules[0].spec_deletable).to be false
120
+ end
121
+
122
+ it 'should accept non-Forge modules with a hash arg' do
123
+ module_opts = { git: 'git@example.com:puppet/test_module.git' }
124
+
125
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, module_opts, anything).and_call_original
126
+
127
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
128
+ expect(subject.modules.collect(&:name)).to include('test_module')
129
+ end
130
+
131
+ it 'should accept non-Forge modules with a valid relative :install_path option' do
132
+ module_opts = {
133
+ install_path: 'vendor',
134
+ git: 'git@example.com:puppet/test_module.git',
135
+ }
136
+
137
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', File.join(basedir, 'vendor'), module_opts, anything).and_call_original
138
+
139
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
140
+ expect(subject.modules.collect(&:name)).to include('test_module')
141
+ end
142
+
143
+ it 'should accept non-Forge modules with a valid absolute :install_path option' do
144
+ install_path = File.join(basedir, 'vendor')
145
+
146
+ module_opts = {
147
+ install_path: install_path,
148
+ git: 'git@example.com:puppet/test_module.git',
149
+ }
150
+
151
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', install_path, module_opts, anything).and_call_original
152
+
153
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
154
+ expect(subject.modules.collect(&:name)).to include('test_module')
155
+ end
156
+
157
+ it 'should reject non-Forge modules with an invalid relative :install_path option' do
158
+ module_opts = {
159
+ install_path: '../../vendor',
160
+ git: 'git@example.com:puppet/test_module.git',
161
+ }
162
+
163
+ expect { subject.add_module('puppet/test_module', module_opts) }.to raise_error(R10K::Error, /cannot manage content.*is not within/i).and not_change { subject.modules }
164
+ end
165
+
166
+ it 'should reject non-Forge modules with an invalid absolute :install_path option' do
167
+ module_opts = {
168
+ install_path: '/tmp/mydata/vendor',
169
+ git: 'git@example.com:puppet/test_module.git',
170
+ }
171
+
172
+ expect { subject.add_module('puppet/test_module', module_opts) }.to raise_error(R10K::Error, /cannot manage content.*is not within/i).and not_change { subject.modules }
173
+ end
174
+
175
+ it 'should disable and not add modules that conflict with the environment' do
176
+ env = instance_double('R10K::Environment::Base')
177
+ mod = instance_double('R10K::Module::Base', name: 'conflict', origin: :puppetfile, 'origin=': nil)
178
+ loader = R10K::ModuleLoader::Puppetfile.new(basedir: basedir, environment: env)
179
+ allow(env).to receive(:'module_conflicts?').with(mod).and_return(true)
180
+ allow(mod).to receive(:spec_deletable=)
181
+
182
+ expect(R10K::Module).to receive(:new).with('conflict', anything, anything, anything).and_return(mod)
183
+ expect { loader.add_module('conflict', {}) }.not_to change { loader.modules }
184
+ end
185
+ end
186
+
187
+ describe '#purge_exclusions' do
188
+ let(:managed_dirs) { ['dir1', 'dir2'] }
189
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir') }
190
+
191
+ it 'includes managed_directories' do
192
+ expect(subject.send(:determine_purge_exclusions, managed_dirs)).to match_array(managed_dirs)
193
+ end
194
+
195
+ context 'when belonging to an environment' do
196
+ let(:env_contents) { ['env1', 'env2' ] }
197
+ let(:env) { double(:environment, desired_contents: env_contents) }
198
+
199
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir', environment: env) }
200
+
201
+ it "includes environment's desired_contents" do
202
+ expect(subject.send(:determine_purge_exclusions, managed_dirs)).to match_array(managed_dirs + env_contents)
203
+ end
204
+ end
205
+ end
206
+
207
+ describe '#managed_directories' do
208
+
209
+ let(:basedir) { '/test/basedir' }
210
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: basedir) }
211
+
212
+ before do
213
+ allow(subject).to receive(:puppetfile_content).and_return('')
214
+ end
215
+
216
+ it 'returns an array of paths that #purge! will operate within' do
217
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '1.2.3'), anything).and_call_original
218
+ subject.add_module('puppet/test_module', '1.2.3')
219
+ subject.load
220
+
221
+ expect(subject.modules.length).to be 1
222
+ expect(subject.managed_directories).to match_array([subject.moduledir])
223
+ end
224
+
225
+ context "with a module with install_path == ''" do
226
+ it "basedir isn't in the list of paths to purge" do
227
+ module_opts = { install_path: '', git: 'git@example.com:puppet/test_module.git' }
228
+
229
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', basedir, module_opts, anything).and_call_original
230
+ subject.add_module('puppet/test_module', module_opts)
231
+ subject.load
232
+
233
+ expect(subject.modules.length).to be 1
234
+ expect(subject.managed_directories).to be_empty
235
+ end
236
+ end
237
+ end
238
+
239
+ describe 'evaluating a Puppetfile' do
240
+ def expect_wrapped_error(error, pf_path, error_type)
241
+ expect(error).to be_a_kind_of(R10K::Error)
242
+ expect(error.message).to eq("Failed to evaluate #{pf_path}")
243
+ expect(error.original).to be_a_kind_of(error_type)
244
+ end
245
+
246
+ subject { described_class.new(basedir: @path) }
247
+
248
+ it 'wraps and re-raises syntax errors' do
249
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'invalid-syntax')
250
+ pf_path = File.join(@path, 'Puppetfile')
251
+ expect {
252
+ subject.load
253
+ }.to raise_error do |e|
254
+ expect_wrapped_error(e, pf_path, SyntaxError)
255
+ end
256
+ end
257
+
258
+ it 'wraps and re-raises load errors' do
259
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'load-error')
260
+ pf_path = File.join(@path, 'Puppetfile')
261
+ expect {
262
+ subject.load
263
+ }.to raise_error do |e|
264
+ expect_wrapped_error(e, pf_path, LoadError)
265
+ end
266
+ end
267
+
268
+ it 'wraps and re-raises argument errors' do
269
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'argument-error')
270
+ pf_path = File.join(@path, 'Puppetfile')
271
+ expect {
272
+ subject.load
273
+ }.to raise_error do |e|
274
+ expect_wrapped_error(e, pf_path, ArgumentError)
275
+ end
276
+ end
277
+
278
+ it 'rejects Puppetfiles with duplicate module names' do
279
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'duplicate-module-error')
280
+ pf_path = File.join(@path, 'Puppetfile')
281
+ expect {
282
+ subject.load
283
+ }.to raise_error(R10K::Error, /Puppetfiles cannot contain duplicate module names/i)
284
+ end
285
+
286
+ it 'wraps and re-raises name errors' do
287
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'name-error')
288
+ pf_path = File.join(@path, 'Puppetfile')
289
+ expect {
290
+ subject.load
291
+ }.to raise_error do |e|
292
+ expect_wrapped_error(e, pf_path, NameError)
293
+ end
294
+ end
295
+
296
+ it 'accepts a forge module with a version' do
297
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'valid-forge-with-version')
298
+ pf_path = File.join(@path, 'Puppetfile')
299
+ expect { subject.load }.not_to raise_error
300
+ end
301
+
302
+ describe 'setting a custom moduledir' do
303
+ it 'allows setting an absolute moduledir' do
304
+ @path = '/fake/basedir'
305
+ allow(subject).to receive(:puppetfile_content).and_return('moduledir "/fake/moduledir"')
306
+ subject.load
307
+ expect(subject.instance_variable_get(:@moduledir)).to eq('/fake/moduledir')
308
+ end
309
+
310
+ it 'roots relative moduledirs in the basedir' do
311
+ @path = '/fake/basedir'
312
+ allow(subject).to receive(:puppetfile_content).and_return('moduledir "my/moduledir"')
313
+ subject.load
314
+ expect(subject.instance_variable_get(:@moduledir)).to eq(File.join(@path, 'my/moduledir'))
315
+ end
316
+ end
317
+
318
+ it 'accepts a forge module without a version' do
319
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'valid-forge-without-version')
320
+ pf_path = File.join(@path, 'Puppetfile')
321
+ expect { subject.load }.not_to raise_error
322
+ end
323
+
324
+ it 'creates a git module and applies the default branch specified in the Puppetfile' do
325
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'default-branch-override')
326
+ pf_path = File.join(@path, 'Puppetfile')
327
+ expect { subject.load }.not_to raise_error
328
+ git_module = subject.modules[0]
329
+ expect(git_module.default_ref).to eq 'here_lies_the_default_branch'
330
+ end
331
+
332
+ it 'creates a git module and applies the provided default_branch_override' do
333
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'default-branch-override')
334
+ pf_path = File.join(@path, 'Puppetfile')
335
+ default_branch_override = 'default_branch_override_name'
336
+ subject.default_branch_override = default_branch_override
337
+ expect { subject.load }.not_to raise_error
338
+ git_module = subject.modules[0]
339
+ expect(git_module.default_override_ref).to eq default_branch_override
340
+ expect(git_module.default_ref).to eq 'here_lies_the_default_branch'
341
+ end
342
+ end
343
+ end