r10k 1.2.4 → 1.3.0rc1

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 (57) hide show
  1. checksums.yaml +8 -8
  2. data/{CHANGELOG → CHANGELOG.mkd} +51 -41
  3. data/doc/dynamic-environments/configuration.mkd +1 -1
  4. data/doc/dynamic-environments/git-environments.markdown +19 -0
  5. data/doc/dynamic-environments/usage.mkd +6 -0
  6. data/lib/r10k/cli/deploy.rb +15 -0
  7. data/lib/r10k/cli/ext/logging.rb +0 -1
  8. data/lib/r10k/cli/module/deploy.rb +0 -1
  9. data/lib/r10k/cli/puppetfile.rb +2 -2
  10. data/lib/r10k/cli.rb +2 -16
  11. data/lib/r10k/deployment/environment.rb +9 -79
  12. data/lib/r10k/deployment/source.rb +15 -89
  13. data/lib/r10k/deployment.rb +13 -14
  14. data/lib/r10k/environment/base.rb +42 -0
  15. data/lib/r10k/environment/git.rb +79 -0
  16. data/lib/r10k/environment/svn.rb +73 -0
  17. data/lib/r10k/environment.rb +7 -0
  18. data/lib/r10k/execution.rb +0 -1
  19. data/lib/r10k/git/cache.rb +11 -5
  20. data/lib/r10k/git/repository.rb +1 -8
  21. data/lib/r10k/git/working_dir.rb +11 -34
  22. data/lib/r10k/git.rb +0 -1
  23. data/lib/r10k/instance_cache.rb +32 -0
  24. data/lib/r10k/keyed_factory.rb +39 -0
  25. data/lib/r10k/module/forge.rb +2 -3
  26. data/lib/r10k/module/svn.rb +0 -1
  27. data/lib/r10k/puppetfile.rb +0 -1
  28. data/lib/r10k/registry.rb +3 -31
  29. data/lib/r10k/source/base.rb +60 -0
  30. data/lib/r10k/source/git.rb +195 -0
  31. data/lib/r10k/source/svn.rb +140 -0
  32. data/lib/r10k/source.rb +39 -0
  33. data/lib/r10k/svn/remote.rb +48 -0
  34. data/lib/r10k/svn/working_dir.rb +0 -2
  35. data/lib/r10k/svn.rb +6 -0
  36. data/lib/r10k/task/deployment.rb +1 -2
  37. data/lib/r10k/task.rb +0 -2
  38. data/lib/r10k/task_runner.rb +0 -1
  39. data/lib/r10k/util/core_ext/hash_ext.rb +19 -0
  40. data/lib/r10k/util/subprocess.rb +0 -1
  41. data/lib/r10k/version.rb +1 -1
  42. data/lib/r10k.rb +1 -0
  43. data/spec/unit/deployment/environment_spec.rb +16 -15
  44. data/spec/unit/environment/git_spec.rb +81 -0
  45. data/spec/unit/environment/svn_spec.rb +76 -0
  46. data/spec/unit/git/repository_spec.rb +0 -10
  47. data/spec/unit/git/working_dir_spec.rb +1 -110
  48. data/spec/unit/{registry_spec.rb → instance_cache_spec.rb} +3 -3
  49. data/spec/unit/keyed_factory_spec.rb +51 -0
  50. data/spec/unit/source/git_spec.rb +274 -0
  51. data/spec/unit/source/svn_spec.rb +102 -0
  52. data/spec/unit/source_spec.rb +10 -0
  53. data/spec/unit/svn/remote_spec.rb +21 -0
  54. data/spec/unit/util/core_ext/hash_ext_spec.rb +63 -0
  55. metadata +36 -10
  56. data/lib/r10k/git/alternates.rb +0 -49
  57. data/spec/unit/git/alternates_spec.rb +0 -90
@@ -0,0 +1,195 @@
1
+ require 'r10k/git'
2
+ require 'r10k/environment'
3
+ require 'r10k/util/purgeable'
4
+ require 'r10k/util/core_ext/hash_ext'
5
+
6
+ # This class implements a source for Git environments.
7
+ #
8
+ # A Git source generates environments by locally caching the given Git
9
+ # repository and enumerating the branches for the Git repository. Branches
10
+ # are mapped to environments without modification.
11
+ #
12
+ # @since 1.3.0
13
+ class R10K::Source::Git < R10K::Source::Base
14
+
15
+ include R10K::Logging
16
+
17
+ R10K::Source.register(:git, self)
18
+ # Register git as the default source
19
+ R10K::Source.register(nil, self)
20
+
21
+ # @!attribute [r] remote
22
+ # @return [String] The URL to the remote git repository
23
+ attr_reader :remote
24
+
25
+ # @!attribute [r] cache
26
+ # @api private
27
+ # @return [R10K::Git::Cache] The git cache associated with this source
28
+ attr_reader :cache
29
+
30
+ # @!attribute [r] settings
31
+ # @return [Hash<Symbol, Object>] Additional settings that configure how
32
+ # the source should behave.
33
+ attr_reader :settings
34
+
35
+ # @!attribute [r] invalid_branches
36
+ # @return [String] How Git branch names that cannot be cleanly mapped to
37
+ # Puppet environments will be handled.
38
+ attr_reader :invalid_branches
39
+
40
+ # Initialize the given source.
41
+ #
42
+ # @param name [String] The identifier for this source.
43
+ # @param basedir [String] The base directory where the generated environments will be created.
44
+ # @param options [Hash] An additional set of options for this source.
45
+ #
46
+ # @option options [Boolean] :prefix Whether to prefix the source name to the
47
+ # environment directory names. Defaults to false.
48
+ # @option options [String] :remote The URL to the base directory of the SVN repository
49
+ # @option options [Hash] :remote Additional settings that configure how the
50
+ # source should behave.
51
+ def initialize(name, basedir, options = {})
52
+ super
53
+
54
+ @environments = []
55
+
56
+ @remote = options[:remote]
57
+ @invalid_branches = (options[:invalid_branches] || 'correct_and_warn')
58
+
59
+ @cache = R10K::Git::Cache.generate(@remote)
60
+ end
61
+
62
+ # Update the git cache for this git source to get the latest list of environments.
63
+ #
64
+ # @return [void]
65
+ def preload!
66
+ logger.info "Determining current branches for #{@remote.inspect}"
67
+ @cache.sync
68
+ end
69
+ alias fetch_remote preload!
70
+
71
+ # Load the git remote and create environments for each branch. If the cache
72
+ # has not been fetched, this will return an empty list.
73
+ #
74
+ # @return [Array<R10K::Environment::Git>]
75
+ def environments
76
+ if not @cache.cached?
77
+ []
78
+ elsif not @environments.empty?
79
+ @environments
80
+ else
81
+ @environments = generate_environments()
82
+ end
83
+ end
84
+
85
+ def generate_environments
86
+ envs = []
87
+ branch_names.each do |bn|
88
+ if bn.valid?
89
+ envs << R10K::Environment::Git.new(bn.name, @basedir, bn.dirname,
90
+ {:remote => remote, :ref => bn.name})
91
+ elsif bn.correct?
92
+ logger.warn "Environment #{bn.name.inspect} contained non-word characters, correcting name to #{bn.dirname}"
93
+ envs << R10K::Environment::Git.new(bn.name, @basedir, bn.dirname,
94
+ {:remote => remote, :ref => bn.name})
95
+ elsif bn.validate?
96
+ logger.error "Environment #{bn.name.inspect} contained non-word characters, ignoring it."
97
+ end
98
+ end
99
+
100
+ envs
101
+ end
102
+
103
+ include R10K::Util::Purgeable
104
+
105
+ def managed_directory
106
+ @basedir
107
+ end
108
+
109
+ def current_contents
110
+ dir = self.managed_directory
111
+ glob_part = @prefix ? @name.to_s() + '_*' : '*'
112
+ glob_exp = File.join(dir, glob_part)
113
+
114
+ Dir.glob(glob_exp).map do |fname|
115
+ File.basename fname
116
+ end
117
+ end
118
+
119
+ # List all environments that should exist in the basedir for this source
120
+ # @note This implements a required method for the Purgeable mixin
121
+ # @return [Array<String>]
122
+ def desired_contents
123
+ @environments.map {|env| env.dirname }
124
+ end
125
+
126
+ private
127
+
128
+ def branch_names
129
+ @cache.branches.map do |branch|
130
+ BranchName.new(branch, {
131
+ :prefix => @prefix,
132
+ :sourcename => @name,
133
+ :invalid => @invalid_branches,
134
+ })
135
+ end
136
+ end
137
+
138
+ # @api private
139
+ class BranchName
140
+
141
+ attr_reader :name
142
+
143
+ INVALID_CHARACTERS = %r[\W]
144
+
145
+ def initialize(name, opts)
146
+ @name = name
147
+ @opts = opts
148
+
149
+ @prefix = opts[:prefix]
150
+ @sourcename = opts[:sourcename]
151
+ @invalid = opts[:invalid]
152
+
153
+ case @invalid
154
+ when 'correct_and_warn'
155
+ @validate = true
156
+ @correct = true
157
+ when 'correct'
158
+ @validate = false
159
+ @correct = true
160
+ when 'error'
161
+ @validate = true
162
+ @correct = false
163
+ when NilClass
164
+ @validate = opts[:validate]
165
+ @correct = opts[:correct]
166
+ end
167
+ end
168
+
169
+ def correct?; @correct end
170
+ def validate?; @validate end
171
+
172
+ def valid?
173
+ if @validate
174
+ ! @name.match(INVALID_CHARACTERS)
175
+ else
176
+ true
177
+ end
178
+ end
179
+
180
+ def dirname
181
+ dir = @name.dup
182
+
183
+ if @prefix
184
+ dir = "#{@sourcename}_#{dir}"
185
+ end
186
+
187
+ if @correct
188
+ dir.gsub!(INVALID_CHARACTERS, '_')
189
+ end
190
+
191
+ dir
192
+ end
193
+
194
+ end
195
+ end
@@ -0,0 +1,140 @@
1
+ require 'r10k/svn'
2
+ require 'r10k/environment'
3
+ require 'r10k/util/purgeable'
4
+ require 'r10k/util/core_ext/hash_ext'
5
+
6
+ # This class implements a source for SVN environments.
7
+ #
8
+ # An SVN source generates environments by enumerating the branches and trunk
9
+ # for a given SVN remote. SVN repositories must conform to the conventional
10
+ # SVN repository structure with the directories trunk/, branches/, and
11
+ # optionally tags/ in the root of the repository. The trunk/ directory is
12
+ # specifically mapped to the production environment, branches are created
13
+ # as environments with the name of the given branch.
14
+ #
15
+ # @see http://svnbook.red-bean.com/en/1.7/svn.branchmerge.maint.html
16
+ # @since 1.3.0
17
+ class R10K::Source::SVN < R10K::Source::Base
18
+
19
+ R10K::Source.register(:svn, self)
20
+
21
+ # @!attribute [r] remote
22
+ # @return [String] The URL to the base directory of the SVN repository
23
+ attr_reader :remote
24
+
25
+ # @!attribute [r] svn_remote
26
+ # @api private
27
+ # @return [R10K::SVN::Remote]
28
+ attr_reader :svn_remote
29
+
30
+ # Initialize the given source.
31
+ #
32
+ # @param name [String] The identifier for this source.
33
+ # @param basedir [String] The base directory where the generated environments will be created.
34
+ # @param options [Hash] An additional set of options for this source.
35
+ #
36
+ # @option options [Boolean] :prefix Whether to prefix the source name to the
37
+ # environment directory names. Defaults to false.
38
+ # @option options [String] :remote The URL to the base directory of the SVN repository
39
+ def initialize(name, basedir, options = {})
40
+ super
41
+
42
+ @remote = options[:remote]
43
+ @environments = []
44
+
45
+ @svn_remote = R10K::SVN::Remote.new(@remote)
46
+ end
47
+
48
+ # Enumerate the environments associated with this SVN source.
49
+ #
50
+ # @return [Array<R10K::Environment::SVN>] An array of environments created
51
+ # from this source.
52
+ def environments
53
+ if @environments.empty?
54
+ @environments = generate_environments()
55
+ end
56
+
57
+ @environments
58
+ end
59
+
60
+ # Generate a list of currently available SVN environments
61
+ #
62
+ # @todo respect environment name corrections
63
+ #
64
+ # @api protected
65
+ # @return [Array<R10K::Environment::SVN>] An array of environments created
66
+ # from this source.
67
+ def generate_environments
68
+
69
+ branch_names.map do |branch|
70
+ R10K::Environment::SVN.new(branch.name, @basedir, branch.dirname,
71
+ { :remote => branch.remote })
72
+ end
73
+ end
74
+
75
+ include R10K::Util::Purgeable
76
+
77
+ def managed_directory
78
+ @basedir
79
+ end
80
+
81
+ def current_contents
82
+ Dir.glob(File.join(@basedir, '*')).map do |fname|
83
+ File.basename fname
84
+ end
85
+ end
86
+
87
+ # List all environments that should exist in the basedir for this source
88
+ # @note This implements a required method for the Purgeable mixin
89
+ # @return [Array<String>]
90
+ def desired_contents
91
+ @environments.map {|env| env.dirname }
92
+ end
93
+
94
+ include R10K::Logging
95
+
96
+ private
97
+
98
+ def branch_names
99
+ branches = []
100
+
101
+ branch_opts = {
102
+ :prefix => @prefix,
103
+ :sourcename => @name,
104
+ }
105
+
106
+ branches << BranchName.new('production', "#{@remote}/trunk", branch_opts)
107
+ @svn_remote.branches.each do |branch|
108
+ branches << BranchName.new(branch, "#{@remote}/branches/#{branch}", branch_opts)
109
+ end
110
+
111
+ branches
112
+ end
113
+
114
+ # @api private
115
+ # @todo respect environment name corrections
116
+ class BranchName
117
+
118
+ attr_reader :name
119
+ attr_reader :remote
120
+
121
+ def initialize(name, remote, opts)
122
+ @name = name
123
+ @remote = remote
124
+ @opts = opts
125
+
126
+ @prefix = opts[:prefix]
127
+ @sourcename = opts[:sourcename]
128
+ end
129
+
130
+ def dirname
131
+ dir = @name.dup
132
+
133
+ if @prefix
134
+ dir = "#{@sourcename}_#{dir}"
135
+ end
136
+
137
+ dir
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,39 @@
1
+ require 'r10k'
2
+ require 'r10k/keyed_factory'
3
+ require 'r10k/util/core_ext/hash_ext'
4
+
5
+ module R10K
6
+ module Source
7
+ def self.factory
8
+ @factory ||= R10K::KeyedFactory.new
9
+ end
10
+
11
+ def self.register(key, klass)
12
+ factory.register(key, klass)
13
+ end
14
+
15
+ def self.retrieve(key)
16
+ factory.retrieve(key)
17
+ end
18
+
19
+ def self.generate(type, basedir, name, options = {})
20
+ factory.generate(type, basedir, name, options)
21
+ end
22
+
23
+ def self.from_hash(name, hash)
24
+ hash.extend R10K::Util::CoreExt::HashExt::SymbolizeKeys
25
+ hash.symbolize_keys!
26
+
27
+ basedir = hash.delete(:basedir)
28
+
29
+ type = hash.delete(:type)
30
+ type = type.is_a?(String) ? type.to_sym : type
31
+
32
+ generate(type, name, basedir, hash)
33
+ end
34
+
35
+ require 'r10k/source/base'
36
+ require 'r10k/source/git'
37
+ require 'r10k/source/svn'
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ require 'r10k/util/subprocess'
2
+
3
+ class R10K::SVN::Remote
4
+
5
+ def initialize(baseurl)
6
+ @baseurl = baseurl
7
+ end
8
+
9
+ # @todo validate that the path to trunk exists in the remote
10
+ def trunk
11
+ "#{@baseurl}/trunk"
12
+ end
13
+
14
+ # @todo gracefully handle cases where no branches exist
15
+ def branches
16
+ text = svn ['ls', "#{@baseurl}/branches"]
17
+ text.lines.map do |line|
18
+ line.chomp!
19
+ line.gsub!(%r[/$], '')
20
+ line
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ include R10K::Logging
27
+
28
+ # Wrap SVN commands
29
+ #
30
+ # @param argv [Array<String>]
31
+ # @param opts [Hash]
32
+ #
33
+ # @option opts [Pathname] :cwd The directory to run the command in
34
+ #
35
+ # @return [String] The stdout from the given command
36
+ def svn(argv, opts = {})
37
+ argv.unshift('svn')
38
+
39
+ subproc = R10K::Util::Subprocess.new(argv)
40
+ subproc.raise_on_fail = true
41
+ subproc.logger = self.logger
42
+
43
+ subproc.cwd = opts[:cwd]
44
+ result = subproc.execute
45
+
46
+ result.stdout
47
+ end
48
+ end
@@ -1,4 +1,3 @@
1
- require 'r10k/logging'
2
1
  require 'r10k/util/subprocess'
3
2
 
4
3
  module R10K
@@ -48,7 +47,6 @@ module R10K
48
47
  svn ["info"], :cwd => @full_path
49
48
  end
50
49
 
51
- include R10K::Execution
52
50
  include R10K::Logging
53
51
 
54
52
  # Wrap SVN commands
data/lib/r10k/svn.rb ADDED
@@ -0,0 +1,6 @@
1
+ module R10K
2
+ module SVN
3
+ require 'r10k/svn/working_dir'
4
+ require 'r10k/svn/remote'
5
+ end
6
+ end
@@ -67,8 +67,7 @@ module Deployment
67
67
  end
68
68
 
69
69
  def call
70
- logger.info "Loading environments from all sources"
71
- @deployment.fetch_sources
70
+ @deployment.preload!
72
71
 
73
72
  with_environments(@environment_names) do |env|
74
73
  task = R10K::Task::Environment::Deploy.new(env)
data/lib/r10k/task.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'r10k/logging'
2
-
3
1
  module R10K
4
2
  module Task
5
3
  class Base
@@ -1,5 +1,4 @@
1
1
  require 'r10k/task'
2
- require 'r10k/logging'
3
2
 
4
3
  module R10K
5
4
  class TaskRunner
@@ -0,0 +1,19 @@
1
+ module R10K
2
+ module Util
3
+ module CoreExt
4
+ module HashExt
5
+ module SymbolizeKeys
6
+ def symbolize_keys!
7
+ self.keys.each do |key|
8
+ next unless key.is_a? String
9
+ if self[key.to_sym]
10
+ raise TypeError, "An existing interned key for #{key} exists, cannot overwrite"
11
+ end
12
+ self[key.to_sym] = self.delete(key)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,3 @@
1
- require 'r10k/logging'
2
1
  require 'r10k/errors'
3
2
  require 'r10k/util/platform'
4
3
 
data/lib/r10k/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module R10K
2
- VERSION = '1.2.4'
2
+ VERSION = '1.3.0rc1'
3
3
  end
data/lib/r10k.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module R10K; end
2
2
 
3
3
  require 'r10k/version'
4
+ require 'r10k/logging'
@@ -2,23 +2,24 @@ require 'spec_helper'
2
2
  require 'r10k/deployment/environment'
3
3
 
4
4
  describe R10K::Deployment::Environment do
5
- let(:remote) { 'git://github.com/adrienthebo/r10k-fixture-repo' }
6
- let(:ref) { 'master' }
7
5
 
8
- describe 'dirname' do
9
- it 'uses the ref as the default dirname' do
10
- subject = described_class.new(ref, remote, '/tmp')
11
- subject.dirname.should == 'master'
12
- end
6
+ it "creates R10K::Environment::Git instances" do
7
+ subject = described_class.new('gitref', 'git://git-server.local/git-remote.git', '/some/nonexistent/dir')
8
+ expect(subject).to be_a_kind_of R10K::Environment::Git
9
+ end
10
+
11
+ it "uses the ref as the dirname by default" do
12
+ subject = described_class.new('gitref', 'git://git-server.local/git-remote.git', '/some/nonexistent/dir')
13
+ expect(subject.dirname).to eq 'gitref'
14
+ end
13
15
 
14
- it 'uses the ref and a provided source name in the default dirname' do
15
- subject = described_class.new(ref, remote, '/tmp', nil, "the")
16
- subject.dirname.should == 'the_master'
17
- end
16
+ it "can specify an explicit dirname" do
17
+ subject = described_class.new('gitref', 'git://git-server.local/git-remote.git', '/some/nonexistent/dir', 'explicit-dirname')
18
+ expect(subject.dirname).to eq 'explicit-dirname'
19
+ end
18
20
 
19
- it 'allows a specific dirname to be set' do
20
- subject = described_class.new(ref, remote, '/tmp', 'sourcename_master')
21
- subject.dirname.should == 'sourcename_master'
22
- end
21
+ it "supports prefixing for backwards compatibility" do
22
+ subject = described_class.new('gitref', 'git://git-server.local/git-remote.git', '/some/nonexistent/dir', nil, 'source')
23
+ expect(subject.dirname).to eq 'source_gitref'
23
24
  end
24
25
  end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ require 'r10k/environment'
3
+
4
+ describe R10K::Environment::Git do
5
+
6
+ subject do
7
+ described_class.new(
8
+ 'myenv',
9
+ '/some/nonexistent/environmentdir',
10
+ 'gitref',
11
+ {
12
+ :remote => 'git://git-server.site/my-repo.git',
13
+ :ref => 'd026ea677116424d2968edb9cee8cbc24d09322b',
14
+ }
15
+ )
16
+ end
17
+
18
+ let(:working_dir) { subject.working_dir }
19
+
20
+ describe "storing attributes" do
21
+ it "can return the environment name" do
22
+ expect(subject.name).to eq 'myenv'
23
+ end
24
+
25
+ it "can return the environment basedir" do
26
+ expect(subject.basedir).to eq '/some/nonexistent/environmentdir'
27
+ end
28
+
29
+ it "can return the environment dirname" do
30
+ expect(subject.dirname).to eq 'gitref'
31
+ end
32
+
33
+ it "can return the environment remote" do
34
+ expect(subject.remote).to eq 'git://git-server.site/my-repo.git'
35
+ end
36
+
37
+ it "can return the environment ref" do
38
+ expect(subject.ref).to eq 'd026ea677116424d2968edb9cee8cbc24d09322b'
39
+ end
40
+ end
41
+
42
+ describe "synchronizing the environment" do
43
+ it "updates all modules when creating a new environment" do
44
+ allow(working_dir).to receive(:cloned?).and_return(false)
45
+ expect(working_dir).to receive(:sync)
46
+ expect(subject).to receive(:sync_modules)
47
+ subject.sync
48
+ end
49
+
50
+ it "does not update all modules when updating an existing environment" do
51
+ allow(working_dir).to receive(:cloned?).and_return(true)
52
+ expect(working_dir).to receive(:sync)
53
+ expect(subject).to_not receive(:sync_modules)
54
+ subject.sync
55
+ end
56
+ end
57
+
58
+ describe "generating a puppetfile for the environment" do
59
+ let(:puppetfile) { subject.puppetfile }
60
+
61
+ it "creates a puppetfile at the full path to the environment" do
62
+ expect(puppetfile.basedir).to eq '/some/nonexistent/environmentdir/gitref'
63
+ end
64
+
65
+ it "sets the moduledir to 'modules' relative to the environment path" do
66
+ expect(puppetfile.moduledir).to eq '/some/nonexistent/environmentdir/gitref/modules'
67
+ end
68
+
69
+ it "sets the puppetfile path to 'Puppetfile' relative to the environment path" do
70
+ expect(puppetfile.puppetfile_path).to eq '/some/nonexistent/environmentdir/gitref/Puppetfile'
71
+ end
72
+ end
73
+
74
+ describe "enumerating modules" do
75
+ it "loads the Puppetfile and returns modules in that puppetfile" do
76
+ expect(subject.puppetfile).to receive(:load)
77
+ expect(subject.puppetfile).to receive(:modules).and_return [:modules]
78
+ expect(subject.modules).to eq([:modules])
79
+ end
80
+ end
81
+ end