r10k 3.4.1 → 3.7.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +4 -1
  3. data/.github/workflows/docker.yml +25 -1
  4. data/.github/workflows/rspec_tests.yml +81 -0
  5. data/.travis.yml +19 -8
  6. data/CHANGELOG.mkd +46 -6
  7. data/Gemfile +1 -1
  8. data/README.mkd +13 -4
  9. data/azure-pipelines.yml +4 -2
  10. data/doc/dynamic-environments/configuration.mkd +49 -3
  11. data/doc/faq.mkd +6 -1
  12. data/doc/puppetfile.mkd +2 -0
  13. data/docker/Makefile +19 -3
  14. data/docker/r10k/Dockerfile +22 -8
  15. data/docker/r10k/release.Dockerfile +23 -4
  16. data/integration/tests/git_source/git_source_repeated_remote.rb +68 -0
  17. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +1 -1
  18. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +1 -1
  19. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +1 -1
  20. data/lib/r10k/action/deploy/environment.rb +6 -1
  21. data/lib/r10k/action/deploy/module.rb +2 -1
  22. data/lib/r10k/action/runner.rb +5 -4
  23. data/lib/r10k/cli/deploy.rb +1 -0
  24. data/lib/r10k/environment/base.rb +1 -1
  25. data/lib/r10k/forge/module_release.rb +2 -2
  26. data/lib/r10k/git/cache.rb +12 -4
  27. data/lib/r10k/git/stateful_repository.rb +4 -0
  28. data/lib/r10k/initializers.rb +1 -0
  29. data/lib/r10k/module/base.rb +8 -0
  30. data/lib/r10k/module/git.rb +4 -0
  31. data/lib/r10k/puppetfile.rb +26 -6
  32. data/lib/r10k/settings.rb +12 -1
  33. data/lib/r10k/source.rb +1 -0
  34. data/lib/r10k/source/exec.rb +51 -0
  35. data/lib/r10k/source/git.rb +22 -2
  36. data/lib/r10k/source/hash.rb +32 -8
  37. data/lib/r10k/version.rb +1 -1
  38. data/locales/r10k.pot +33 -10
  39. data/spec/shared-examples/subprocess-runner.rb +11 -5
  40. data/spec/unit/action/deploy/environment_spec.rb +9 -0
  41. data/spec/unit/action/deploy/module_spec.rb +15 -2
  42. data/spec/unit/action/puppetfile/install_spec.rb +4 -1
  43. data/spec/unit/action/runner_spec.rb +2 -2
  44. data/spec/unit/forge/module_release_spec.rb +14 -10
  45. data/spec/unit/git/cache_spec.rb +10 -0
  46. data/spec/unit/git/rugged/credentials_spec.rb +1 -1
  47. data/spec/unit/git_spec.rb +3 -3
  48. data/spec/unit/puppetfile_spec.rb +67 -2
  49. data/spec/unit/settings_spec.rb +12 -0
  50. data/spec/unit/source/exec_spec.rb +81 -0
  51. data/spec/unit/source/git_spec.rb +49 -1
  52. data/spec/unit/source/hash_spec.rb +54 -0
  53. data/spec/unit/source/yaml_spec.rb +42 -0
  54. metadata +8 -2
@@ -12,6 +12,8 @@ module R10K
12
12
  class << self
13
13
  # Path to puppet executable
14
14
  attr_accessor :puppet_path
15
+ # Path to puppet.conf
16
+ attr_accessor :puppet_conf
15
17
  end
16
18
 
17
19
  def self.git_settings
@@ -131,6 +133,15 @@ module R10K
131
133
  end
132
134
  end
133
135
  }),
136
+ Definition.new(:puppet_conf, {
137
+ :desc => "Path to puppet.conf. Defaults to /etc/puppetlabs/puppet/puppet.conf.",
138
+ :default => '/etc/puppetlabs/puppet/puppet.conf',
139
+ :validate => lambda do |value|
140
+ unless File.readable? value
141
+ raise ArgumentError, "The specified puppet.conf #{value} is not readable"
142
+ end
143
+ end
144
+ }),
134
145
  ])
135
146
  end
136
147
 
@@ -160,7 +171,7 @@ module R10K
160
171
 
161
172
  Definition.new(:pool_size, {
162
173
  :desc => "The amount of threads used to concurrently install modules. The default value is 1: install one module at a time.",
163
- :default => 1,
174
+ :default => 4,
164
175
  :validate => lambda do |value|
165
176
  if !value.is_a?(Integer)
166
177
  raise ArgumentError, "The pool_size setting should be an integer, not a #{value.class}"
@@ -37,5 +37,6 @@ module R10K
37
37
  require 'r10k/source/svn'
38
38
  require 'r10k/source/yaml'
39
39
  require 'r10k/source/yamldir'
40
+ require 'r10k/source/exec'
40
41
  end
41
42
  end
@@ -0,0 +1,51 @@
1
+ require 'r10k/util/subprocess'
2
+ require 'json'
3
+ require 'yaml'
4
+
5
+ class R10K::Source::Exec < R10K::Source::Hash
6
+ R10K::Source.register(:exec, self)
7
+
8
+ def initialize(name, basedir, options = {})
9
+ unless @command = options[:command]
10
+ raise ConfigError, _('Environment source %{name} missing required parameter: command') % {name: name}
11
+ end
12
+
13
+ # We haven't set the environments option yet. We will do that by
14
+ # overloading the #environments method.
15
+ super(name, basedir, options)
16
+ end
17
+
18
+ def environments_hash
19
+ @environments_hash ||= set_environments_hash(run_environments_command)
20
+ end
21
+
22
+ private
23
+
24
+ def run_environments_command
25
+ subproc = R10K::Util::Subprocess.new([@command])
26
+ subproc.raise_on_fail = true
27
+ subproc.logger = self.logger
28
+ procresult = subproc.execute
29
+
30
+ begin
31
+ environments = JSON.parse(procresult.stdout)
32
+ rescue JSON::ParserError => json_err
33
+ begin
34
+ environments = YAML.safe_load(procresult.stdout)
35
+ rescue Psych::SyntaxError => yaml_err
36
+ raise R10K::Error, _("Error parsing command output for exec source %{name}:\n" \
37
+ "Not valid JSON: %{j_msg}\n" \
38
+ "Not valid YAML: %{y_msg}\n" \
39
+ "Stdout:\n%{out}") % {name: name, j_msg: json_err.message, y_msg: yaml_err.message, out: procresult.stdout}
40
+ end
41
+ end
42
+
43
+ unless R10K::Source::Hash.valid_environments_hash?(environments)
44
+ raise R10K::Error, _("Environment source %{name} command %{cmd} did not return valid environment data.\n" \
45
+ 'Returned: %{data}') % {name: name, cmd: @command, data: environments}
46
+ end
47
+
48
+ # Return the resulting environments hash
49
+ environments
50
+ end
51
+ end
@@ -41,6 +41,10 @@ class R10K::Source::Git < R10K::Source::Base
41
41
  # that will be deployed as environments.
42
42
  attr_reader :ignore_branch_prefixes
43
43
 
44
+ # @!attribute [r] filter_command
45
+ # @return [String] Command to run to filter branches
46
+ attr_reader :filter_command
47
+
44
48
  # Initialize the given source.
45
49
  #
46
50
  # @param name [String] The identifier for this source.
@@ -61,6 +65,7 @@ class R10K::Source::Git < R10K::Source::Base
61
65
  @remote = options[:remote]
62
66
  @invalid_branches = (options[:invalid_branches] || 'correct_and_warn')
63
67
  @ignore_branch_prefixes = options[:ignore_branch_prefixes]
68
+ @filter_command = options[:filter_command]
64
69
 
65
70
  @cache = R10K::Git.cache.generate(@remote)
66
71
  end
@@ -115,7 +120,7 @@ class R10K::Source::Git < R10K::Source::Base
115
120
  environments.map {|env| env.dirname }
116
121
  end
117
122
 
118
- def filter_branches(branches, ignore_prefixes)
123
+ def filter_branches_by_regexp(branches, ignore_prefixes)
119
124
  filter = Regexp.new("^#{Regexp.union(ignore_prefixes)}")
120
125
  branches = branches.reject do |branch|
121
126
  result = filter.match(branch)
@@ -127,14 +132,29 @@ class R10K::Source::Git < R10K::Source::Base
127
132
  branches
128
133
  end
129
134
 
135
+ def filter_branches_by_command(branches, command)
136
+ branches.select do |branch|
137
+ result = system({'GIT_DIR' => @cache.git_dir.to_s, 'R10K_BRANCH' => branch, 'R10K_NAME' => @name.to_s}, command)
138
+ unless result
139
+ logger.warn _("Branch `%{name}:%{branch}` filtered out by filter_command %{cmd}") % {name: @name, branch: branch, cmd: command}
140
+ end
141
+ result
142
+ end
143
+ end
144
+
130
145
  private
131
146
 
132
147
  def branch_names
133
148
  opts = {:prefix => @prefix, :invalid => @invalid_branches, :source => @name}
134
149
  branches = @cache.branches
135
150
  if @ignore_branch_prefixes && !@ignore_branch_prefixes.empty?
136
- branches = filter_branches(branches, @ignore_branch_prefixes)
151
+ branches = filter_branches_by_regexp(branches, @ignore_branch_prefixes)
152
+ end
153
+
154
+ if @filter_command && !@filter_command.empty?
155
+ branches = filter_branches_by_command(branches, @filter_command)
137
156
  end
157
+
138
158
  branches.map do |branch|
139
159
  R10K::Environment::Name.new(branch, opts)
140
160
  end
@@ -120,6 +120,16 @@
120
120
  #
121
121
  class R10K::Source::Hash < R10K::Source::Base
122
122
 
123
+ include R10K::Logging
124
+
125
+ # @param hash [Hash] A hash to validate.
126
+ # @return [Boolean] False if the hash is obviously invalid. A true return
127
+ # means _maybe_ it's valid.
128
+ def self.valid_environments_hash?(hash)
129
+ # TODO: more robust schema valiation
130
+ hash.is_a?(Hash)
131
+ end
132
+
123
133
  # @param name [String] The identifier for this source.
124
134
  # @param basedir [String] The base directory where the generated environments will be created.
125
135
  # @param options [Hash] An additional set of options for this source. The
@@ -131,19 +141,33 @@ class R10K::Source::Hash < R10K::Source::Base
131
141
  # @option options [Hash] :environments The hash definition of environments
132
142
  def initialize(name, basedir, options = {})
133
143
  super(name, basedir, options)
144
+ end
134
145
 
135
- @environments_hash = options.delete(:environments) || {}
136
-
137
- @environments_hash.keys.each do |name|
138
- # TODO: deal with names that aren't valid
139
- ::R10K::Util::SymbolizeKeys.symbolize_keys!(@environments_hash[name])
140
- @environments_hash[name][:basedir] = basedir
141
- @environments_hash[name][:dirname] = name
146
+ # Set the environment hash for the source. The environment hash is what the
147
+ # source uses to generate enviroments.
148
+ # @param hash [Hash] The hash to sanitize and use as the source's environments.
149
+ # Should be formatted for use with R10K::Environment#from_hash.
150
+ def set_environments_hash(hash)
151
+ @environments_hash = hash.reduce({}) do |memo,(name,opts)|
152
+ R10K::Util::SymbolizeKeys.symbolize_keys!(opts)
153
+ memo.merge({
154
+ name => opts.merge({
155
+ :basedir => @basedir,
156
+ :dirname => R10K::Environment::Name.new(name, {prefix: @prefix, source: @name}).dirname
157
+ })
158
+ })
142
159
  end
143
160
  end
144
161
 
162
+ # Return the sanitized environments hash for this source. The environments
163
+ # hash should contain objects formatted for use with R10K::Environment#from_hash.
164
+ # If the hash does not exist it will be built based on @options.
165
+ def environments_hash
166
+ @environments_hash ||= set_environments_hash(@options.fetch(:environments, {}))
167
+ end
168
+
145
169
  def environments
146
- @environments ||= @environments_hash.map do |name, hash|
170
+ @environments ||= environments_hash.map do |name, hash|
147
171
  R10K::Environment.from_hash(name, hash)
148
172
  end
149
173
  end
@@ -2,5 +2,5 @@ module R10K
2
2
  # When updating to a new major (X) or minor (Y) version, include `#major` or
3
3
  # `#minor` (respectively) in your commit message to trigger the appropriate
4
4
  # release. Otherwise, a new patch (Z) version will be released.
5
- VERSION = '3.4.1'
5
+ VERSION = '3.7.0'
6
6
  end
@@ -1,16 +1,16 @@
1
1
  # SOME DESCRIPTIVE TITLE.
2
- # Copyright (C) 2019 Puppet, Inc.
2
+ # Copyright (C) 2020 Puppet, Inc.
3
3
  # This file is distributed under the same license as the r10k package.
4
- # FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
5
5
  #
6
6
  #, fuzzy
7
7
  msgid ""
8
8
  msgstr ""
9
- "Project-Id-Version: r10k 3.3.3-22-g3035320\n"
9
+ "Project-Id-Version: r10k 3.4.1-57-g2eb088a\n"
10
10
  "\n"
11
11
  "Report-Msgid-Bugs-To: docs@puppetlabs.com\n"
12
- "POT-Creation-Date: 2019-12-17 14:55+0000\n"
13
- "PO-Revision-Date: 2019-12-17 14:55+0000\n"
12
+ "POT-Creation-Date: 2020-07-22 16:41+0000\n"
13
+ "PO-Revision-Date: 2020-07-22 16:41+0000\n"
14
14
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
15
15
  "Language-Team: LANGUAGE <LL@li.org>\n"
16
16
  "Language: \n"
@@ -423,26 +423,49 @@ msgstr ""
423
423
  msgid "Setting %{name} requires a URL but '%{value}' could not be parsed as a URL"
424
424
  msgstr ""
425
425
 
426
- #: ../lib/r10k/source/git.rb:72
426
+ #: ../lib/r10k/source/exec.rb:10
427
+ msgid "Environment source %{name} missing required parameter: command"
428
+ msgstr ""
429
+
430
+ #: ../lib/r10k/source/exec.rb:36
431
+ msgid ""
432
+ "Error parsing command output for exec source %{name}:\n"
433
+ "Not valid JSON: %{j_msg}\n"
434
+ "Not valid YAML: %{y_msg}\n"
435
+ "Stdout:\n"
436
+ "%{out}"
437
+ msgstr ""
438
+
439
+ #: ../lib/r10k/source/exec.rb:44
440
+ msgid ""
441
+ "Environment source %{name} command %{cmd} did not return valid environment data.\n"
442
+ "Returned: %{data}"
443
+ msgstr ""
444
+
445
+ #: ../lib/r10k/source/git.rb:77
427
446
  msgid "Fetching '%{remote}' to determine current branches."
428
447
  msgstr ""
429
448
 
430
- #: ../lib/r10k/source/git.rb:75
449
+ #: ../lib/r10k/source/git.rb:80
431
450
  msgid "Unable to determine current branches for Git source '%{name}' (%{basedir})"
432
451
  msgstr ""
433
452
 
434
- #: ../lib/r10k/source/git.rb:100
453
+ #: ../lib/r10k/source/git.rb:105
435
454
  msgid "Environment %{env_name} contained non-word characters, correcting name to %{corrected_env_name}"
436
455
  msgstr ""
437
456
 
438
- #: ../lib/r10k/source/git.rb:104
457
+ #: ../lib/r10k/source/git.rb:109
439
458
  msgid "Environment %{env_name} contained non-word characters, ignoring it."
440
459
  msgstr ""
441
460
 
442
- #: ../lib/r10k/source/git.rb:123 ../lib/r10k/source/svn.rb:113
461
+ #: ../lib/r10k/source/git.rb:128 ../lib/r10k/source/svn.rb:113
443
462
  msgid "Branch %{branch} filtered out by ignore_branch_prefixes %{ibp}"
444
463
  msgstr ""
445
464
 
465
+ #: ../lib/r10k/source/git.rb:139
466
+ msgid "Branch `%{name}:%{branch}` filtered out by filter_command %{cmd}"
467
+ msgstr ""
468
+
446
469
  #: ../lib/r10k/source/yaml.rb:10
447
470
  msgid "Couldn't open environments file %{file}: %{err}"
448
471
  msgstr ""
@@ -32,23 +32,29 @@ shared_examples_for "a subprocess runner" do |fixture_root|
32
32
  end
33
33
  end
34
34
 
35
- describe "running 'ls' with a different working directory" do
35
+ describe "running 'ls' or 'dir' with a different working directory" do
36
36
  subject do
37
- described_class.new(%w[ls]).tap do |o|
38
- o.cwd = fixture_root
37
+ if R10K::Util::Platform.windows?
38
+ described_class.new(%w[cmd /c dir]).tap do |o|
39
+ o.cwd = fixture_root
40
+ end
41
+ else
42
+ described_class.new(%w[ls]).tap do |o|
43
+ o.cwd = fixture_root
44
+ end
39
45
  end
40
46
  end
41
47
 
42
48
  it "returns the contents of the given working directory" do
43
49
  result = subject.run
44
- expect(result.stdout).to eq 'no-execute.sh'
50
+ expect(result.stdout).to match('no-execute.sh')
45
51
  end
46
52
  end
47
53
 
48
54
  describe "running 'false'" do
49
55
  subject { described_class.new(%w[false]) }
50
56
 
51
- it "sets the exit code to 1" do
57
+ it "sets the exit code to 1", unless: R10K::Util::Platform.windows? do
52
58
  result = subject.run
53
59
  expect(result.exit_code).to eq 1
54
60
  end
@@ -331,6 +331,15 @@ describe R10K::Action::Deploy::Environment do
331
331
  expect(subject.instance_variable_get(:@puppet_path)).to eq('/nonexistent')
332
332
  end
333
333
  end
334
+
335
+ describe 'with puppet-conf' do
336
+
337
+ subject { described_class.new({ config: '/some/nonexistent/path', 'puppet-conf': '/nonexistent' }, []) }
338
+
339
+ it 'sets puppet_conf' do
340
+ expect(subject.instance_variable_get(:@puppet_conf)).to eq('/nonexistent')
341
+ end
342
+ end
334
343
  end
335
344
 
336
345
  describe "write_environment_info!" do
@@ -26,6 +26,10 @@ describe R10K::Action::Deploy::Module do
26
26
  described_class.new({ 'puppet-path': '/nonexistent' }, [])
27
27
  end
28
28
 
29
+ it 'can accept a puppet-conf option' do
30
+ described_class.new({ 'puppet-conf': '/nonexistent' }, [])
31
+ end
32
+
29
33
  it 'can accept a cachedir option' do
30
34
  described_class.new({ cachedir: '/nonexistent' }, [])
31
35
  end
@@ -70,8 +74,8 @@ describe R10K::Action::Deploy::Module do
70
74
 
71
75
  before do
72
76
  allow(subject).to receive(:visit_environment).and_wrap_original do |original, environment, &block|
73
- expect(environment.puppetfile).to receive(:modules).and_return(
74
- [R10K::Module::Local.new(environment.name, '/fakedir', [], environment)]
77
+ expect(environment.puppetfile).to receive(:modules_by_vcs_cachedir).and_return(
78
+ {none: [R10K::Module::Local.new(environment.name, '/fakedir', [], environment)]}
75
79
  )
76
80
  original.call(environment, &block)
77
81
  end
@@ -128,6 +132,15 @@ describe R10K::Action::Deploy::Module do
128
132
  end
129
133
  end
130
134
 
135
+ describe 'with puppet-conf' do
136
+
137
+ subject { described_class.new({ config: '/some/nonexistent/path', 'puppet-conf': '/nonexistent' }, []) }
138
+
139
+ it 'sets puppet_conf' do
140
+ expect(subject.instance_variable_get(:@puppet_conf)).to eq('/nonexistent')
141
+ end
142
+ end
143
+
131
144
  describe 'with cachedir' do
132
145
 
133
146
  subject { described_class.new({ config: '/some/nonexistent/path', cachedir: '/nonexistent' }, []) }
@@ -19,12 +19,15 @@ describe R10K::Action::Puppetfile::Install do
19
19
 
20
20
  describe "installing modules" do
21
21
  let(:modules) do
22
- Array.new(4, R10K::Module::Base.new('author/modname', "/some/nonexistent/path/modname", nil))
22
+ (1..4).map do |idx|
23
+ R10K::Module::Base.new("author/modname#{idx}", "/some/nonexistent/path/modname#{idx}", nil)
24
+ end
23
25
  end
24
26
 
25
27
  before do
26
28
  allow(puppetfile).to receive(:purge!)
27
29
  allow(puppetfile).to receive(:modules).and_return(modules)
30
+ allow(puppetfile).to receive(:modules_by_vcs_cachedir).and_return({none: modules})
28
31
  end
29
32
 
30
33
  it "syncs each module in the Puppetfile" do
@@ -94,7 +94,7 @@ describe R10K::Action::Runner do
94
94
  else
95
95
  { "#{conf_path}": override }
96
96
  end
97
- expect(global_settings).to receive(:evaluate).with(overrides).and_call_original
97
+ expect(global_settings).to receive(:evaluate).with(hash_including(overrides)).and_call_original
98
98
  runner.call
99
99
  end
100
100
  end
@@ -109,7 +109,7 @@ describe R10K::Action::Runner do
109
109
  else
110
110
  { "#{conf_path}": override }
111
111
  end
112
- expect(global_settings).to receive(:evaluate).with(overrides).and_call_original
112
+ expect(global_settings).to receive(:evaluate).with(hash_including(overrides)).and_call_original
113
113
  runner.call
114
114
  end
115
115
  end
@@ -166,33 +166,37 @@ describe R10K::Forge::ModuleRelease do
166
166
  end
167
167
 
168
168
  describe "#cleanup_unpack_path" do
169
- it "ignores the unpack_path if it doesn't exist" do
170
- expect(unpack_path).to receive(:exist?).and_return false
171
- expect(unpack_path).to_not receive(:parent)
169
+ it "ignores the unpack_path if the parent doesn't exist" do
170
+ parent = instance_double('Pathname')
171
+ expect(parent).to receive(:exist?).and_return false
172
+ expect(parent).to_not receive(:rmtree)
173
+ expect(unpack_path).to receive(:parent).and_return(parent)
172
174
  subject.cleanup_unpack_path
173
175
  end
174
176
 
175
177
  it "removes the containing directory of unpack_path if it exists" do
176
178
  parent = instance_double('Pathname')
177
179
  expect(parent).to receive(:rmtree)
178
- expect(unpack_path).to receive(:exist?).and_return true
179
- expect(unpack_path).to receive(:parent).and_return(parent)
180
+ expect(parent).to receive(:exist?).and_return true
181
+ expect(unpack_path).to receive(:parent).and_return(parent).exactly(2).times
180
182
  subject.cleanup_unpack_path
181
183
  end
182
184
  end
183
185
 
184
186
  describe "#cleanup_download_path" do
185
- it "ignores the download_path if it doesn't exist" do
186
- expect(download_path).to receive(:exist?).and_return false
187
- expect(download_path).to_not receive(:parent)
187
+ it "ignores the download_path if the parent doesn't exist" do
188
+ parent = instance_double('Pathname')
189
+ expect(parent).to receive(:exist?).and_return false
190
+ expect(parent).to_not receive(:rmtree)
191
+ expect(download_path).to receive(:parent).and_return(parent)
188
192
  subject.cleanup_download_path
189
193
  end
190
194
 
191
195
  it "removes the containing directory of download_path if it exists" do
192
196
  parent = instance_double('Pathname')
193
197
  expect(parent).to receive(:rmtree)
194
- expect(download_path).to receive(:exist?).and_return true
195
- expect(download_path).to receive(:parent).and_return(parent)
198
+ expect(parent).to receive(:exist?).and_return true
199
+ expect(download_path).to receive(:parent).and_return(parent).exactly(2).times
196
200
  subject.cleanup_download_path
197
201
  end
198
202
  end