r10k 3.9.3 → 3.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) 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 +33 -0
  5. data/README.mkd +6 -0
  6. data/doc/dynamic-environments/configuration.mkd +25 -0
  7. data/doc/dynamic-environments/usage.mkd +26 -0
  8. data/doc/puppetfile.mkd +18 -5
  9. data/integration/Rakefile +3 -1
  10. data/integration/tests/basic_functionality/basic_deployment.rb +176 -0
  11. data/integration/tests/user_scenario/basic_workflow/negative/neg_specify_deleted_forge_module.rb +3 -9
  12. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +21 -25
  13. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  14. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  15. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  16. data/lib/r10k/action/base.rb +1 -1
  17. data/lib/r10k/action/deploy/deploy_helpers.rb +4 -0
  18. data/lib/r10k/action/deploy/display.rb +1 -1
  19. data/lib/r10k/action/deploy/environment.rb +22 -9
  20. data/lib/r10k/action/deploy/module.rb +41 -11
  21. data/lib/r10k/action/puppetfile/check.rb +7 -5
  22. data/lib/r10k/action/puppetfile/install.rb +22 -16
  23. data/lib/r10k/action/puppetfile/purge.rb +12 -9
  24. data/lib/r10k/action/runner.rb +45 -10
  25. data/lib/r10k/action/visitor.rb +3 -0
  26. data/lib/r10k/cli/deploy.rb +5 -0
  27. data/lib/r10k/cli/puppetfile.rb +0 -1
  28. data/lib/r10k/content_synchronizer.rb +16 -4
  29. data/lib/r10k/environment/base.rb +64 -11
  30. data/lib/r10k/environment/with_modules.rb +6 -10
  31. data/lib/r10k/git/cache.rb +1 -1
  32. data/lib/r10k/git/rugged/credentials.rb +77 -0
  33. data/lib/r10k/git/stateful_repository.rb +8 -0
  34. data/lib/r10k/git.rb +3 -0
  35. data/lib/r10k/initializers.rb +4 -0
  36. data/lib/r10k/module/base.rb +42 -1
  37. data/lib/r10k/module/definition.rb +64 -0
  38. data/lib/r10k/module/forge.rb +16 -3
  39. data/lib/r10k/module/git.rb +23 -1
  40. data/lib/r10k/module/local.rb +6 -3
  41. data/lib/r10k/module/svn.rb +11 -0
  42. data/lib/r10k/module.rb +20 -2
  43. data/lib/r10k/module_loader/puppetfile/dsl.rb +42 -0
  44. data/lib/r10k/module_loader/puppetfile.rb +276 -0
  45. data/lib/r10k/puppetfile.rb +82 -160
  46. data/lib/r10k/settings/definition.rb +1 -1
  47. data/lib/r10k/settings.rb +58 -2
  48. data/lib/r10k/source/base.rb +10 -0
  49. data/lib/r10k/source/git.rb +5 -0
  50. data/lib/r10k/source/svn.rb +4 -0
  51. data/lib/r10k/util/purgeable.rb +74 -8
  52. data/lib/r10k/util/setopts.rb +2 -0
  53. data/lib/r10k/util/subprocess.rb +1 -0
  54. data/lib/r10k/version.rb +1 -1
  55. data/locales/r10k.pot +165 -65
  56. data/r10k.gemspec +2 -0
  57. data/spec/fixtures/unit/action/r10k_forge_auth.yaml +4 -0
  58. data/spec/fixtures/unit/action/r10k_forge_auth_no_url.yaml +3 -0
  59. data/spec/fixtures/unit/puppetfile/forge-override/Puppetfile +8 -0
  60. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile +10 -0
  61. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile.new +10 -0
  62. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_subdir_2/ignored_1 +0 -0
  63. data/spec/fixtures/unit/util/purgeable/managed_two/.hidden/unmanaged_3 +0 -0
  64. data/spec/r10k-mocks/mock_env.rb +3 -0
  65. data/spec/r10k-mocks/mock_source.rb +7 -3
  66. data/spec/unit/action/deploy/environment_spec.rb +105 -30
  67. data/spec/unit/action/deploy/module_spec.rb +232 -42
  68. data/spec/unit/action/puppetfile/check_spec.rb +17 -5
  69. data/spec/unit/action/puppetfile/install_spec.rb +42 -36
  70. data/spec/unit/action/puppetfile/purge_spec.rb +15 -17
  71. data/spec/unit/action/runner_spec.rb +122 -26
  72. data/spec/unit/environment/base_spec.rb +30 -17
  73. data/spec/unit/environment/git_spec.rb +2 -2
  74. data/spec/unit/environment/svn_spec.rb +4 -3
  75. data/spec/unit/environment/with_modules_spec.rb +2 -1
  76. data/spec/unit/git/cache_spec.rb +14 -0
  77. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  78. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  79. data/spec/unit/module/base_spec.rb +54 -8
  80. data/spec/unit/module/forge_spec.rb +59 -5
  81. data/spec/unit/module/git_spec.rb +67 -9
  82. data/spec/unit/module/svn_spec.rb +35 -5
  83. data/spec/unit/module_loader/puppetfile_spec.rb +405 -0
  84. data/spec/unit/module_spec.rb +12 -1
  85. data/spec/unit/puppetfile_spec.rb +125 -189
  86. data/spec/unit/settings_spec.rb +47 -2
  87. data/spec/unit/util/purgeable_spec.rb +38 -6
  88. metadata +29 -3
@@ -23,6 +23,7 @@ class R10K::Environment::WithModules < R10K::Environment::Base
23
23
  def initialize(name, basedir, dirname, options = {})
24
24
  super
25
25
 
26
+ @all_modules = nil
26
27
  @managed_content = {}
27
28
  @modules = []
28
29
  @moduledir = case options[:moduledir]
@@ -43,10 +44,12 @@ class R10K::Environment::WithModules < R10K::Environment::Base
43
44
  # - The r10k environment object
44
45
  # - A Puppetfile in the environment's content
45
46
  def modules
46
- return @modules if puppetfile.nil?
47
+ if @all_modules.nil?
48
+ puppetfile_modules = super()
49
+ @all_modules = @modules + puppetfile_modules
50
+ end
47
51
 
48
- puppetfile.load unless puppetfile.loaded?
49
- @modules + puppetfile.modules
52
+ @all_modules
50
53
  end
51
54
 
52
55
  def module_conflicts?(mod_b)
@@ -126,13 +129,6 @@ class R10K::Environment::WithModules < R10K::Environment::Base
126
129
 
127
130
  include R10K::Util::Purgeable
128
131
 
129
- # Returns an array of the full paths that can be purged.
130
- # @note This implements a required method for the Purgeable mixin
131
- # @return [Array<String>]
132
- def managed_directories
133
- [@full_path]
134
- end
135
-
136
132
  # Returns an array of the full paths of filenames that should exist. Files
137
133
  # inside managed_directories that are not listed in desired_contents will
138
134
  # be purged.
@@ -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
@@ -35,6 +35,7 @@ class R10K::Git::StatefulRepository
35
35
  @cache.resolve(ref)
36
36
  end
37
37
 
38
+ # Returns true if the sync actually updated the repo, false otherwise
38
39
  def sync(ref, force=true)
39
40
  @cache.sync if sync_cache?(ref)
40
41
 
@@ -46,6 +47,7 @@ class R10K::Git::StatefulRepository
46
47
 
47
48
  workdir_status = status(ref)
48
49
 
50
+ updated = true
49
51
  case workdir_status
50
52
  when :absent
51
53
  logger.debug(_("Cloning %{repo_path} and checking out %{ref}") % {repo_path: @repo.path, ref: ref })
@@ -64,15 +66,20 @@ class R10K::Git::StatefulRepository
64
66
  @repo.checkout(sha, {:force => force})
65
67
  else
66
68
  logger.warn(_("Skipping %{repo_path} due to local modifications") % {repo_path: @repo.path})
69
+ updated = false
67
70
  end
68
71
  else
69
72
  logger.debug(_("%{repo_path} is already at Git ref %{ref}") % {repo_path: @repo.path, ref: ref })
73
+ updated = false
70
74
  end
75
+ updated
71
76
  end
72
77
 
73
78
  def status(ref)
74
79
  if !@repo.exist?
75
80
  :absent
81
+ elsif !@cache.exist?
82
+ :mismatched
76
83
  elsif !@repo.git_dir.exist?
77
84
  :mismatched
78
85
  elsif !@repo.git_dir.directory?
@@ -93,6 +100,7 @@ class R10K::Git::StatefulRepository
93
100
  # @api private
94
101
  def sync_cache?(ref)
95
102
  return true if !@cache.exist?
103
+ return true if ref == 'HEAD'
96
104
  return true if !([:commit, :tag].include? @cache.ref_type(ref))
97
105
  return false
98
106
  end
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, {}
@@ -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,7 @@ 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| PuppetForge::Connection.authorization = value }
66
70
  end
67
71
  end
68
72
  end
@@ -1,9 +1,12 @@
1
1
  require 'r10k/module'
2
+ require 'r10k/logging'
2
3
  require 'puppet_forge'
3
4
 
4
5
  # This class defines a common interface for module implementations.
5
6
  class R10K::Module::Base
6
7
 
8
+ include R10K::Logging
9
+
7
10
  # @!attribute [r] title
8
11
  # @return [String] The forward slash separated owner and name of the module
9
12
  attr_reader :title
@@ -35,6 +38,10 @@ class R10K::Module::Base
35
38
  # @return [String] Where the module was sourced from. E.g., "Puppetfile"
36
39
  attr_accessor :origin
37
40
 
41
+ # @!attribute [rw] spec_deletable
42
+ # @return [Boolean] set this to true if the spec dir can be safely removed, ie in the moduledir
43
+ attr_accessor :spec_deletable
44
+
38
45
  # There's been some churn over `author` vs `owner` and `full_name` over
39
46
  # `title`, so in the short run it's easier to support both and deprecate one
40
47
  # later.
@@ -43,7 +50,7 @@ class R10K::Module::Base
43
50
 
44
51
  # @param title [String]
45
52
  # @param dirname [String]
46
- # @param args [Array]
53
+ # @param args [Hash]
47
54
  def initialize(title, dirname, args, environment=nil)
48
55
  @title = PuppetForge::V3.normalize_name(title)
49
56
  @dirname = dirname
@@ -52,6 +59,9 @@ class R10K::Module::Base
52
59
  @path = Pathname.new(File.join(@dirname, @name))
53
60
  @environment = environment
54
61
  @overrides = args.delete(:overrides) || {}
62
+ @spec_deletable = true
63
+ @exclude_spec = args.delete(:exclude_spec)
64
+ @exclude_spec = @overrides[:modules].delete(:exclude_spec) if @overrides.dig(:modules, :exclude_spec)
55
65
  @origin = 'external' # Expect Puppetfile or R10k::Environment to set this to a specific value
56
66
 
57
67
  @requested_modules = @overrides.dig(:modules, :requested_modules) || []
@@ -64,8 +74,39 @@ class R10K::Module::Base
64
74
  path.to_s
65
75
  end
66
76
 
77
+ # Delete the spec dir if @exclude_spec has been set to true and @spec_deletable is also true
78
+ def maybe_delete_spec_dir
79
+ if @exclude_spec
80
+ if @spec_deletable
81
+ delete_spec_dir
82
+ else
83
+ logger.info _("Spec dir for #{@title} will not be deleted because it is not in the moduledir")
84
+ end
85
+ end
86
+ end
87
+
88
+ # Actually remove the spec dir
89
+ def delete_spec_dir
90
+ spec_path = @path + 'spec'
91
+ if spec_path.symlink?
92
+ spec_path = spec_path.realpath
93
+ end
94
+ if spec_path.directory?
95
+ logger.debug2 _("Deleting spec data at #{spec_path}")
96
+ # Use the secure flag for the #rm_rf method to avoid security issues
97
+ # involving TOCTTOU(time of check to time of use); more details here:
98
+ # https://ruby-doc.org/stdlib-2.7.0/libdoc/fileutils/rdoc/FileUtils.html#method-c-rm_rf
99
+ # Additionally, #rm_rf also has problems in windows with with symlink targets
100
+ # also being deleted; this should be revisted if Windows becomes higher priority.
101
+ FileUtils.rm_rf(spec_path, secure: true)
102
+ else
103
+ logger.debug2 _("No spec dir detected at #{spec_path}, skipping deletion")
104
+ end
105
+ end
106
+
67
107
  # Synchronize this module with the indicated state.
68
108
  # @param [Hash] opts Deprecated
109
+ # @return [Boolean] true if the module was updated, false otherwise
69
110
  def sync(opts={})
70
111
  raise NotImplementedError
71
112
  end
@@ -0,0 +1,64 @@
1
+ require 'r10k/module'
2
+
3
+ class R10K::Module::Definition < R10K::Module::Base
4
+
5
+ attr_reader :version
6
+
7
+ def initialize(name, dirname:, args:, implementation:, environment: nil)
8
+ @original_name = name
9
+ @original_args = args.dup
10
+ @implementation = implementation
11
+ @version = implementation.statically_defined_version(name, args)
12
+
13
+ super(name, dirname, args, environment)
14
+ end
15
+
16
+ def to_implementation
17
+ mod = @implementation.new(@title, @dirname, @original_args, @environment)
18
+
19
+ mod.origin = origin
20
+ mod.spec_deletable = spec_deletable
21
+
22
+ mod
23
+ end
24
+
25
+ # syncing is a noop for module definitions
26
+ # Returns false to inidicate the module was not updated
27
+ def sync(args = {})
28
+ logger.debug1(_("Not updating module %{name}, assuming content unchanged") % {name: name})
29
+ false
30
+ end
31
+
32
+ def status
33
+ :insync
34
+ end
35
+
36
+ def properties
37
+ type = nil
38
+
39
+ if @args[:type]
40
+ type = @args[:type]
41
+ elsif @args[:ref] || @args[:commit] || @args[:branch] || @args[:tag]
42
+ type = 'git'
43
+ elsif @args[:svn]
44
+ # This logic is clear and included for completeness sake, though at
45
+ # this time module definitions do not support SVN versions.
46
+ type = 'svn'
47
+ else
48
+ type = 'forge'
49
+ end
50
+
51
+ {
52
+ expected: version,
53
+ # We can't get the value for `actual` here because that requires the
54
+ # implementation (and potentially expensive operations by the
55
+ # implementation). Some consumers will check this value, if it exists
56
+ # and if not, fall back to the expected version. That is the correct
57
+ # behavior when assuming modules are unchanged, and why `actual` is set
58
+ # to `nil` here.
59
+ actual: nil,
60
+ type: type
61
+ }
62
+ end
63
+ end
64
+
@@ -25,6 +25,10 @@ class R10K::Module::Forge < R10K::Module::Base
25
25
  expected_version == :latest || expected_version.nil? || PuppetForge::Util.version_valid?(expected_version)
26
26
  end
27
27
 
28
+ def self.statically_defined_version(name, args)
29
+ args[:version] if args[:version].is_a?(String)
30
+ end
31
+
28
32
  # @!attribute [r] metadata
29
33
  # @api private
30
34
  # @return [PuppetForge::Metadata]
@@ -35,8 +39,6 @@ class R10K::Module::Forge < R10K::Module::Base
35
39
  # @return [PuppetForge::V3::Module] The Puppet Forge module metadata
36
40
  attr_reader :v3_module
37
41
 
38
- include R10K::Logging
39
-
40
42
  include R10K::Util::Setopts
41
43
 
42
44
  def initialize(title, dirname, opts, environment=nil)
@@ -58,17 +60,24 @@ class R10K::Module::Forge < R10K::Module::Base
58
60
  end
59
61
 
60
62
  # @param [Hash] opts Deprecated
63
+ # @return [Boolean] true if the module was updated, false otherwise
61
64
  def sync(opts={})
65
+ updated = false
62
66
  if should_sync?
63
67
  case status
64
68
  when :absent
65
69
  install
70
+ updated = true
66
71
  when :outdated
67
72
  upgrade
73
+ updated = true
68
74
  when :mismatched
69
75
  reinstall
76
+ updated = true
70
77
  end
78
+ maybe_delete_spec_dir
71
79
  end
80
+ updated
72
81
  end
73
82
 
74
83
  def properties
@@ -83,7 +92,11 @@ class R10K::Module::Forge < R10K::Module::Base
83
92
  def expected_version
84
93
  if @expected_version == :latest
85
94
  begin
86
- @expected_version = @v3_module.current_release.version
95
+ if @v3_module.current_release
96
+ @expected_version = @v3_module.current_release.version
97
+ else
98
+ raise PuppetForge::ReleaseNotFound, _("The module %{title} does not appear to have any published releases, cannot determine latest version.") % { title: @title }
99
+ end
87
100
  rescue Faraday::ResourceNotFound => e
88
101
  raise PuppetForge::ReleaseNotFound, _("The module %{title} does not exist on %{url}.") % {title: @title, url: PuppetForge::V3::Release.conn.url_prefix}, e.backtrace
89
102
  end
@@ -13,6 +13,21 @@ class R10K::Module::Git < R10K::Module::Base
13
13
  false
14
14
  end
15
15
 
16
+ # Will be called if self.implement? above returns true. Will return
17
+ # the version info, if version is statically defined in the modules
18
+ # declaration.
19
+ def self.statically_defined_version(name, args)
20
+ if !args[:type] && (args[:ref] || args[:tag] || args[:commit])
21
+ if args[:ref] && args[:ref].to_s.match(/[0-9a-f]{40}/)
22
+ args[:ref]
23
+ else
24
+ args[:tag] || args[:commit]
25
+ end
26
+ elsif args[:type].to_s == 'git' && args[:version] && args[:version].to_s.match(/[0-9a-f]{40}/)
27
+ args[:version]
28
+ end
29
+ end
30
+
16
31
  # @!attribute [r] repo
17
32
  # @api private
18
33
  # @return [R10K::Git::StatefulRepository]
@@ -83,9 +98,16 @@ class R10K::Module::Git < R10K::Module::Base
83
98
  end
84
99
 
85
100
  # @param [Hash] opts Deprecated
101
+ # @return [Boolean] true if the module was updated, false otherwise
86
102
  def sync(opts={})
87
103
  force = opts[:force] || @force
88
- @repo.sync(version, force) if should_sync?
104
+ if should_sync?
105
+ updated = @repo.sync(version, force)
106
+ else
107
+ updated = false
108
+ end
109
+ maybe_delete_spec_dir
110
+ updated
89
111
  end
90
112
 
91
113
  def status
@@ -1,5 +1,4 @@
1
1
  require 'r10k/module'
2
- require 'r10k/logging'
3
2
 
4
3
  # A dummy module type that can be used to "protect" Puppet modules that exist
5
4
  # inside of the Puppetfile "moduledir" location. Local modules will not be
@@ -12,10 +11,12 @@ class R10K::Module::Local < R10K::Module::Base
12
11
  args.is_a?(Hash) && (args[:local] || args[:type].to_s == 'local')
13
12
  end
14
13
 
15
- include R10K::Logging
14
+ def self.statically_defined_version(*)
15
+ "0.0.0"
16
+ end
16
17
 
17
18
  def version
18
- "0.0.0"
19
+ self.class.statically_defined_version
19
20
  end
20
21
 
21
22
  def properties
@@ -31,7 +32,9 @@ class R10K::Module::Local < R10K::Module::Base
31
32
  end
32
33
 
33
34
  # @param [Hash] opts Deprecated
35
+ # @return [Boolean] false, because local modules are always considered in-sync
34
36
  def sync(opts={})
35
37
  logger.debug1 _("Module %{title} is a local module, always indicating synced.") % {title: title}
38
+ false
36
39
  end
37
40
  end
@@ -10,6 +10,10 @@ class R10K::Module::SVN < R10K::Module::Base
10
10
  args.has_key?(:svn) || args[:type].to_s == 'svn'
11
11
  end
12
12
 
13
+ def self.statically_defined_version(name, args)
14
+ nil
15
+ end
16
+
13
17
  # @!attribute [r] expected_revision
14
18
  # @return [String] The SVN revision that the repo should have checked out
15
19
  attr_reader :expected_revision
@@ -70,17 +74,24 @@ class R10K::Module::SVN < R10K::Module::Base
70
74
  end
71
75
 
72
76
  # @param [Hash] opts Deprecated
77
+ # @return [Boolean] true if the module was updated, false otherwise
73
78
  def sync(opts={})
79
+ updated = false
74
80
  if should_sync?
75
81
  case status
76
82
  when :absent
77
83
  install
84
+ updated = true
78
85
  when :mismatched
79
86
  reinstall
87
+ updated = true
80
88
  when :outdated
81
89
  update
90
+ updated = true
82
91
  end
92
+ maybe_delete_spec_dir
83
93
  end
94
+ updated
84
95
  end
85
96
 
86
97
  def exist?
data/lib/r10k/module.rb CHANGED
@@ -22,17 +22,35 @@ module R10K::Module
22
22
  #
23
23
  # @return [Object < R10K::Module] A member of the implementing subclass
24
24
  def self.new(name, basedir, args, environment=nil)
25
+ with_implementation(name, args) do |implementation|
26
+ implementation.new(name, basedir, args, environment)
27
+ end
28
+ end
29
+
30
+ # Takes the same signature as Module.new but returns an metadata module
31
+ def self.from_metadata(name, basedir, args, environment=nil)
32
+ with_implementation(name, args) do |implementation|
33
+ R10K::Module::Definition.new(name,
34
+ dirname: basedir,
35
+ args: args,
36
+ implementation: implementation,
37
+ environment: environment)
38
+ end
39
+ end
40
+
41
+ def self.with_implementation(name, args, &block)
25
42
  if implementation = @klasses.find { |klass| klass.implement?(name, args) }
26
- obj = implementation.new(name, basedir, args, environment)
27
- obj
43
+ block.call(implementation)
28
44
  else
29
45
  raise _("Module %{name} with args %{args} doesn't have an implementation. (Are you using the right arguments?)") % {name: name, args: args.inspect}
30
46
  end
31
47
  end
32
48
 
49
+
33
50
  require 'r10k/module/base'
34
51
  require 'r10k/module/git'
35
52
  require 'r10k/module/svn'
36
53
  require 'r10k/module/local'
37
54
  require 'r10k/module/forge'
55
+ require 'r10k/module/definition'
38
56
  end
@@ -0,0 +1,42 @@
1
+ module R10K
2
+ module ModuleLoader
3
+ class Puppetfile
4
+ class DSL
5
+ # A barebones implementation of the Puppetfile DSL
6
+ #
7
+ # @api private
8
+
9
+ def initialize(librarian, metadata_only: false)
10
+ @librarian = librarian
11
+ @metadata_only = metadata_only
12
+ end
13
+
14
+ def mod(name, args = nil)
15
+ if args.is_a?(Hash)
16
+ opts = args
17
+ else
18
+ opts = { version: args }
19
+ end
20
+
21
+ if @metadata_only
22
+ @librarian.add_module_metadata(name, opts)
23
+ else
24
+ @librarian.add_module(name, opts)
25
+ end
26
+ end
27
+
28
+ def forge(location)
29
+ @librarian.set_forge(location)
30
+ end
31
+
32
+ def moduledir(location)
33
+ @librarian.set_moduledir(location)
34
+ end
35
+
36
+ def method_missing(method, *args)
37
+ raise NoMethodError, _("unrecognized declaration '%{method}'") % {method: method}
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end