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
@@ -18,14 +18,18 @@ module R10K
18
18
  # @param opts [Hash] A hash of options defined in #allowed_initialized_opts
19
19
  # and managed by the SetOps mixin within the Action::Base class.
20
20
  # Corresponds to the CLI flags and options.
21
- # @param argv [CRI::ArgumentList] A list-like collection of the remaining
22
- # arguments to the CLI invocation (after removing flags and options).
21
+ # @param argv [Enumerable] Typically CRI::ArgumentList or Array. A list-like
22
+ # collection of the remaining arguments to the CLI invocation (after
23
+ # removing flags and options).
23
24
  # @param settings [Hash] A hash of configuration loaded from the relevant
24
25
  # config (r10k.yaml).
25
- def initialize(opts, argv, settings)
26
+ #
27
+ # @note All arguments will be required in the next major version
28
+ def initialize(opts, argv, settings = {})
26
29
  super
27
30
 
28
31
  requested_env = @opts[:environment] ? [@opts[:environment].gsub(/\W/, '_')] : []
32
+ @modified_envs = []
29
33
 
30
34
  @settings = @settings.merge({
31
35
  overrides: {
@@ -34,6 +38,7 @@ module R10K
34
38
  generate_types: @generate_types
35
39
  },
36
40
  modules: {
41
+ exclude_spec: settings.dig(:deploy, :exclude_spec),
37
42
  requested_modules: @argv.map.to_a,
38
43
  # force here is used to make it easier to reason about
39
44
  force: !@no_force
@@ -66,6 +71,21 @@ module R10K
66
71
 
67
72
  def visit_deployment(deployment)
68
73
  yield
74
+ ensure
75
+ if (postcmd = @settings[:postrun])
76
+ if @modified_envs.any?
77
+ if postcmd.grep('$modifiedenvs').any?
78
+ envs_to_run = @modified_envs.join(' ')
79
+ logger.debug "Running postrun command for environments #{envs_to_run}."
80
+ postcmd = postcmd.map { |e| e.gsub('$modifiedenvs', envs_to_run) }
81
+ end
82
+ subproc = R10K::Util::Subprocess.new(postcmd)
83
+ subproc.logger = logger
84
+ subproc.execute
85
+ else
86
+ logger.debug "No environments were modified, not executing postrun command."
87
+ end
88
+ end
69
89
  end
70
90
 
71
91
  def visit_source(source)
@@ -82,10 +102,15 @@ module R10K
82
102
  environment.deploy
83
103
 
84
104
  requested_mods = @settings.dig(:overrides, :modules, :requested_modules) || []
85
- generate_types = @settings.dig(:overrides, :environments, :generate_types)
86
- if generate_types && !((environment.modules.map(&:name) & requested_mods).empty?)
87
- logger.debug("Generating puppet types for environment '#{environment.dirname}'...")
88
- environment.generate_types!
105
+ # We actually synced a module in this env
106
+ if !((environment.modules.map(&:name) & requested_mods).empty?)
107
+ # Record modified environment for postrun command
108
+ @modified_envs << environment.dirname
109
+
110
+ if generate_types = @settings.dig(:overrides, :environments, :generate_types)
111
+ logger.debug("Generating puppet types for environment '#{environment.dirname}'...")
112
+ environment.generate_types!
113
+ end
89
114
  end
90
115
  end
91
116
  end
@@ -93,12 +118,16 @@ module R10K
93
118
  def allowed_initialize_opts
94
119
  super.merge(environment: true,
95
120
  cachedir: :self,
121
+ 'exclude-spec': :self,
96
122
  'no-force': :self,
97
123
  'generate-types': :self,
98
124
  'puppet-path': :self,
99
125
  'puppet-conf': :self,
100
126
  'private-key': :self,
101
- 'oauth-token': :self)
127
+ 'oauth-token': :self,
128
+ 'github-app-id': :self,
129
+ 'github-app-key': :self,
130
+ 'github-app-ttl': :self)
102
131
  end
103
132
  end
104
133
  end
@@ -44,10 +44,13 @@ module R10K
44
44
 
45
45
  overrides = {}
46
46
  overrides[:cachedir] = @opts[:cachedir] if @opts.key?(:cachedir)
47
- overrides[:deploy] = {} if @opts.key?(:'puppet-path') || @opts.key?(:'generate-types')
48
- overrides[:deploy][:puppet_path] = @opts[:'puppet-path'] if @opts.key?(:'puppet-path')
49
- overrides[:deploy][:puppet_conf] = @opts[:'puppet-conf'] unless @opts[:'puppet-conf'].nil?
50
- overrides[:deploy][:generate_types] = @opts[:'generate-types'] if @opts.key?(:'generate-types')
47
+ if @opts.key?(:'puppet-path') || @opts.key?(:'generate-types') || @opts.key?(:'exclude-spec') || @opts.key?(:'puppet-conf')
48
+ overrides[:deploy] = {}
49
+ overrides[:deploy][:puppet_path] = @opts[:'puppet-path'] if @opts.key?(:'puppet-path')
50
+ overrides[:deploy][:puppet_conf] = @opts[:'puppet-conf'] if @opts.key?(:'puppet-conf')
51
+ overrides[:deploy][:generate_types] = @opts[:'generate-types'] if @opts.key?(:'generate-types')
52
+ overrides[:deploy][:exclude_spec] = @opts[:'exclude-spec'] if @opts.key?(:'exclude-spec')
53
+ end
51
54
 
52
55
  with_overrides = config_settings.merge(overrides) do |key, oldval, newval|
53
56
  newval = oldval.merge(newval) if oldval.is_a? Hash
@@ -67,15 +70,20 @@ module R10K
67
70
  exit(8)
68
71
  end
69
72
 
73
+ # Set up authorization from license file if it wasn't
74
+ # already set via the config
70
75
  def setup_authorization
71
- begin
72
- license = R10K::Util::License.load
76
+ if PuppetForge::Connection.authorization.nil?
77
+ begin
78
+ license = R10K::Util::License.load
73
79
 
74
- if license.respond_to?(:authorization_token)
75
- PuppetForge::Connection.authorization = license.authorization_token
80
+ if license.respond_to?(:authorization_token)
81
+ logger.debug "Using token from license to connect to the Forge."
82
+ PuppetForge::Connection.authorization = license.authorization_token
83
+ end
84
+ rescue R10K::Error => e
85
+ logger.warn e.message
76
86
  end
77
- rescue R10K::Error => e
78
- logger.warn e.message
79
87
  end
80
88
  end
81
89
 
@@ -100,11 +108,26 @@ module R10K
100
108
  def add_credential_overrides(overrides)
101
109
  sshkey_path = @opts[:'private-key']
102
110
  token_path = @opts[:'oauth-token']
111
+ app_id = @opts[:'github-app-id']
112
+ app_private_key_path = @opts[:'github-app-key']
113
+ app_ttl = @opts[:'github-app-ttl']
103
114
 
104
115
  if sshkey_path && token_path
105
116
  raise R10K::Error, "Cannot specify both an SSH key and a token to use with this deploy."
106
117
  end
107
118
 
119
+ if sshkey_path && (app_private_key_path || app_id)
120
+ raise R10K::Error, "Cannot specify both an SSH key and an SSL key or Github App id to use with this deploy."
121
+ end
122
+
123
+ if token_path && (app_private_key_path || app_id)
124
+ raise R10K::Error, "Cannot specify both an OAuth token and an SSL key or Github App id to use with this deploy."
125
+ end
126
+
127
+ if app_id && ! app_private_key_path || app_private_key_path && ! app_id
128
+ raise R10K::Error, "Must specify both id and SSL private key to use Github App for this deploy."
129
+ end
130
+
108
131
  if sshkey_path
109
132
  overrides[:git] ||= {}
110
133
  overrides[:git][:private_key] = sshkey_path
@@ -121,6 +144,18 @@ module R10K
121
144
  repo[:oauth_token] = token_path
122
145
  end
123
146
  end
147
+ elsif app_id
148
+ overrides[:git] ||= {}
149
+ overrides[:git][:github_app_id] = app_id
150
+ overrides[:git][:github_app_key] = app_private_key_path
151
+ overrides[:git][:github_app_ttl] = app_ttl
152
+ if repo_settings = overrides[:git][:repositories]
153
+ repo_settings.each do |repo|
154
+ repo[:github_app_id] = app_id
155
+ repo[:github_app_key] = app_private_key_path
156
+ repo[:github_app_ttl] = app_ttl
157
+ end
158
+ end
124
159
  end
125
160
 
126
161
  overrides
@@ -24,6 +24,7 @@ module R10K::CLI
24
24
  option nil, :cachedir, 'Specify a cachedir, overriding the value in config', argument: :required
25
25
  flag nil, :'no-force', 'Prevent the overwriting of local module modifications'
26
26
  flag nil, :'generate-types', 'Run `puppet generate types` after updating an environment'
27
+ flag nil, :'exclude-spec', 'Exclude the module\'s spec dir from deployment'
27
28
  option nil, :'puppet-path', 'Path to puppet executable', argument: :required do |value, cmd|
28
29
  unless File.executable? value
29
30
  $stderr.puts "The specified puppet executable #{value} is not executable."
@@ -34,6 +35,9 @@ module R10K::CLI
34
35
  option nil, :'puppet-conf', 'Path to puppet.conf', argument: :required
35
36
  option nil, :'private-key', 'Path to SSH key to use when cloning. Only valid with rugged provider', argument: :required
36
37
  option nil, :'oauth-token', 'Path to OAuth token to use when cloning. Only valid with rugged provider', argument: :required
38
+ option nil, :'github-app-id', 'Github App id. Only valid with rugged provider', argument: :required
39
+ option nil, :'github-app-key', 'Github App private key. Only valid with rugged provider', argument: :required
40
+ option nil, :'github-app-ttl', 'Github App token expiration, in seconds. Only valid with rugged provider', default: "120", argument: :optional
37
41
 
38
42
  run do |opts, args, cmd|
39
43
  puts cmd.help(:verbose => opts[:verbose])
data/lib/r10k/git.rb CHANGED
@@ -135,6 +135,9 @@ module R10K
135
135
 
136
136
  def_setting_attr :private_key
137
137
  def_setting_attr :oauth_token
138
+ def_setting_attr :github_app_id
139
+ def_setting_attr :github_app_key
140
+ def_setting_attr :github_app_ttl
138
141
  def_setting_attr :proxy
139
142
  def_setting_attr :username
140
143
  def_setting_attr :repositories, {}
@@ -111,6 +111,6 @@ class R10K::Git::Cache
111
111
 
112
112
  # Reformat the remote name into something that can be used as a directory
113
113
  def sanitized_dirname
114
- @sanitized_dirname ||= @remote.gsub(/[^@\w\.-]/, '-')
114
+ @sanitized_dirname ||= @remote.gsub(/(\w+:\/\/)(.*)(@)/, '\1').gsub(/[^@\w\.-]/, '-')
115
115
  end
116
116
  end
@@ -1,6 +1,10 @@
1
1
  require 'r10k/git/rugged'
2
2
  require 'r10k/git/errors'
3
3
  require 'r10k/logging'
4
+ require 'json'
5
+ require 'jwt'
6
+ require 'net/http'
7
+ require 'openssl'
4
8
 
5
9
  # Generate credentials for secured remote connections.
6
10
  #
@@ -62,15 +66,29 @@ class R10K::Git::Rugged::Credentials
62
66
 
63
67
  def get_plaintext_credentials(url, username_from_url)
64
68
  per_repo_oauth_token = nil
69
+ per_repo_github_app_id = nil
70
+ per_repo_github_app_key = nil
71
+ per_repo_github_app_ttl = nil
72
+
65
73
  if per_repo_settings = R10K::Git.get_repo_settings(url)
66
74
  per_repo_oauth_token = per_repo_settings[:oauth_token]
75
+ per_repo_github_app_id = per_repo_settings[:github_app_id]
76
+ per_repo_github_app_key = per_repo_settings[:github_app_key]
77
+ per_repo_github_app_ttl = per_repo_settings[:github_app_ttl]
67
78
  end
68
79
 
80
+ app_id = per_repo_github_app_id || R10K::Git.settings[:github_app_id]
81
+ app_key = per_repo_github_app_key || R10K::Git.settings[:github_app_key]
82
+ app_ttl = per_repo_github_app_ttl || R10K::Git.settings[:github_app_ttl]
83
+
69
84
  if token_path = per_repo_oauth_token || R10K::Git.settings[:oauth_token]
70
85
  @oauth_token ||= extract_token(token_path, url)
71
86
 
72
87
  user = 'x-oauth-token'
73
88
  password = @oauth_token
89
+ elsif app_id && app_key && app_ttl
90
+ user = 'x-access-token'
91
+ password = github_app_token(app_id, app_key, app_ttl)
74
92
  else
75
93
  user = get_git_username(url, username_from_url)
76
94
  password = URI.parse(url).password || ''
@@ -125,4 +143,63 @@ class R10K::Git::Rugged::Credentials
125
143
 
126
144
  user
127
145
  end
146
+
147
+ def github_app_token(app_id, private_key, ttl)
148
+ raise R10K::Git::GitError, _('Github App id contains invalid characters.') unless app_id =~ /^\d+$/
149
+ raise R10K::Git::GitError, _('Github App token ttl contains invalid characters.') unless ttl =~ /^\d+$/
150
+ raise R10K::Git::GitError, _('Github App key is missing or unreadable') unless File.readable?(private_key)
151
+
152
+ begin
153
+ ssl_key = OpenSSL::PKey::RSA.new(File.read(private_key).strip)
154
+ unless ssl_key.private?
155
+ raise R10K::Git::GitError, _('Github App key is not a valid SSL private key')
156
+ end
157
+ rescue OpenSSL::PKey::RSAError
158
+ raise R10K::Git::GitError, _('Github App key is not a valid SSL key')
159
+ end
160
+
161
+ logger.debug2 _("Using Github App id %{app_id} with SSL key from %{key_path}") % { key_path: private_key, app_id: app_id }
162
+
163
+ jwt_issue_time = Time.now.to_i - 60
164
+ jwt_exp_time = (jwt_issue_time + 60) + ttl.to_i
165
+ payload = { iat: jwt_issue_time, exp: jwt_exp_time, iss: app_id }
166
+ jwt = JWT.encode(payload, ssl_key, "RS256")
167
+
168
+ get = URI.parse("https://api.github.com/app/installations")
169
+ get_request = Net::HTTP::Get.new(get)
170
+ get_request["Authorization"] = "Bearer #{jwt}"
171
+ get_request["Accept"] = "application/vnd.github.v3+json"
172
+ get_req_options = { use_ssl: get.scheme == "https", }
173
+ get_response = Net::HTTP.start(get.hostname, get.port, get_req_options) do |http|
174
+ http.request(get_request)
175
+ end
176
+
177
+ unless (get_response.class < Net::HTTPSuccess)
178
+ logger.debug2 _("Unexpected response code: #{get_response.code}\nResponse body: #{get_response.body}")
179
+ raise R10K::Git::GitError, _("Error using private key to get Github App access token from url")
180
+ end
181
+
182
+ access_tokens_url = JSON.parse(get_response.body)[0]['access_tokens_url']
183
+
184
+ post = URI.parse(access_tokens_url)
185
+ post_request = Net::HTTP::Post.new(post)
186
+ post_request["Authorization"] = "Bearer #{jwt}"
187
+ post_request["Accept"] = "application/vnd.github.v3+json"
188
+ post_req_options = { use_ssl: post.scheme == "https", }
189
+ post_response = Net::HTTP.start(post.hostname, post.port, post_req_options) do |http|
190
+ http.request(post_request)
191
+ end
192
+
193
+ unless (post_response.class < Net::HTTPSuccess)
194
+ logger.debug2 _("Unexpected response code: #{post_response.code}\nResponse body: #{post_response.body}")
195
+ raise R10K::Git::GitError, _("Error using private key to generate access token from #{access_token_url}")
196
+ end
197
+
198
+ token = JSON.parse(post_response.body)['token']
199
+
200
+ raise R10K::Git::GitError, _("Github App token contains invalid characters.") unless valid_token?(token)
201
+
202
+ logger.debug2 _("Github App token generated, expires at: %{expire}") % {expire: JSON.parse(post_response.body)['expires_at']}
203
+ token
204
+ end
128
205
  end
@@ -93,6 +93,7 @@ class R10K::Git::StatefulRepository
93
93
  # @api private
94
94
  def sync_cache?(ref)
95
95
  return true if !@cache.exist?
96
+ return true if ref == 'HEAD'
96
97
  return true if !([:commit, :tag].include? @cache.ref_type(ref))
97
98
  return false
98
99
  end
@@ -56,6 +56,9 @@ module R10K
56
56
  with_setting(:proxy) { |value| R10K::Git.settings[:proxy] = value }
57
57
  with_setting(:repositories) { |value| R10K::Git.settings[:repositories] = value }
58
58
  with_setting(:oauth_token) { |value| R10K::Git.settings[:oauth_token] = value }
59
+ with_setting(:github_app_id) { |value| R10K::Git.settings[:github_app_id] = value }
60
+ with_setting(:github_app_key) { |value| R10K::Git.settings[:github_app_key] = value }
61
+ with_setting(:github_app_ttl) { |value| R10K::Git.settings[:github_app_ttl] = value }
59
62
  end
60
63
  end
61
64
 
@@ -63,6 +66,13 @@ module R10K
63
66
  def call
64
67
  with_setting(:baseurl) { |value| PuppetForge.host = value }
65
68
  with_setting(:proxy) { |value| PuppetForge::Connection.proxy = value }
69
+ with_setting(:authorization_token) { |value|
70
+ if @settings[:baseurl]
71
+ PuppetForge::Connection.authorization = value
72
+ else
73
+ raise R10K::Error, "Cannot specify a Forge authorization token without configuring a custom baseurl."
74
+ end
75
+ }
66
76
  end
67
77
  end
68
78
  end
@@ -35,6 +35,10 @@ class R10K::Module::Base
35
35
  # @return [String] Where the module was sourced from. E.g., "Puppetfile"
36
36
  attr_accessor :origin
37
37
 
38
+ # @!attribute [rw] spec_deletable
39
+ # @return [Boolean] set this to true if the spec dir can be safely removed, ie in the moduledir
40
+ attr_accessor :spec_deletable
41
+
38
42
  # There's been some churn over `author` vs `owner` and `full_name` over
39
43
  # `title`, so in the short run it's easier to support both and deprecate one
40
44
  # later.
@@ -52,6 +56,9 @@ class R10K::Module::Base
52
56
  @path = Pathname.new(File.join(@dirname, @name))
53
57
  @environment = environment
54
58
  @overrides = args.delete(:overrides) || {}
59
+ @spec_deletable = true
60
+ @exclude_spec = args.delete(:exclude_spec)
61
+ @exclude_spec = @overrides[:modules].delete(:exclude_spec) if @overrides.dig(:modules, :exclude_spec)
55
62
  @origin = 'external' # Expect Puppetfile or R10k::Environment to set this to a specific value
56
63
 
57
64
  @requested_modules = @overrides.dig(:modules, :requested_modules) || []
@@ -64,6 +71,36 @@ class R10K::Module::Base
64
71
  path.to_s
65
72
  end
66
73
 
74
+ # Delete the spec dir if @exclude_spec has been set to true and @spec_deletable is also true
75
+ def maybe_delete_spec_dir
76
+ if @exclude_spec
77
+ if @spec_deletable
78
+ delete_spec_dir
79
+ else
80
+ logger.info _("Spec dir for #{@title} will not be deleted because it is not in the moduledir")
81
+ end
82
+ end
83
+ end
84
+
85
+ # Actually remove the spec dir
86
+ def delete_spec_dir
87
+ spec_path = @path + 'spec'
88
+ if spec_path.symlink?
89
+ spec_path = spec_path.realpath
90
+ end
91
+ if spec_path.directory?
92
+ logger.debug2 _("Deleting spec data at #{spec_path}")
93
+ # Use the secure flag for the #rm_rf method to avoid security issues
94
+ # involving TOCTTOU(time of check to time of use); more details here:
95
+ # https://ruby-doc.org/stdlib-2.7.0/libdoc/fileutils/rdoc/FileUtils.html#method-c-rm_rf
96
+ # Additionally, #rm_rf also has problems in windows with with symlink targets
97
+ # also being deleted; this should be revisted if Windows becomes higher priority.
98
+ FileUtils.rm_rf(spec_path, secure: true)
99
+ else
100
+ logger.debug2 _("No spec dir detected at #{spec_path}, skipping deletion")
101
+ end
102
+ end
103
+
67
104
  # Synchronize this module with the indicated state.
68
105
  # @param [Hash] opts Deprecated
69
106
  def sync(opts={})
@@ -50,7 +50,7 @@ class R10K::Module::Forge < R10K::Module::Base
50
50
  :version => :expected_version,
51
51
  :source => ::R10K::Util::Setopts::Ignore,
52
52
  :type => ::R10K::Util::Setopts::Ignore,
53
- })
53
+ }, :raise_on_unhandled => false)
54
54
 
55
55
  @expected_version ||= current_version || :latest
56
56
 
@@ -68,6 +68,7 @@ class R10K::Module::Forge < R10K::Module::Base
68
68
  when :mismatched
69
69
  reinstall
70
70
  end
71
+ maybe_delete_spec_dir
71
72
  end
72
73
  end
73
74
 
@@ -83,7 +84,11 @@ class R10K::Module::Forge < R10K::Module::Base
83
84
  def expected_version
84
85
  if @expected_version == :latest
85
86
  begin
86
- @expected_version = @v3_module.current_release.version
87
+ if @v3_module.current_release
88
+ @expected_version = @v3_module.current_release.version
89
+ else
90
+ raise PuppetForge::ReleaseNotFound, _("The module %{title} does not appear to have any published releases, cannot determine latest version.") % { title: @title }
91
+ end
87
92
  rescue Faraday::ResourceNotFound => e
88
93
  raise PuppetForge::ReleaseNotFound, _("The module %{title} does not exist on %{url}.") % {title: @title, url: PuppetForge::V3::Release.conn.url_prefix}, e.backtrace
89
94
  end
@@ -52,7 +52,7 @@ class R10K::Module::Git < R10K::Module::Base
52
52
  :git => :remote,
53
53
  :default_branch => :default_ref,
54
54
  :default_branch_override => :default_override_ref,
55
- })
55
+ }, :raise_on_unhandled => false)
56
56
 
57
57
  force = @overrides.dig(:modules, :force)
58
58
  @force = force == false ? false : true
@@ -86,6 +86,7 @@ class R10K::Module::Git < R10K::Module::Base
86
86
  def sync(opts={})
87
87
  force = opts[:force] || @force
88
88
  @repo.sync(version, force) if should_sync?
89
+ maybe_delete_spec_dir
89
90
  end
90
91
 
91
92
  def status