r10k 1.2.4 → 1.3.0rc1

Sign up to get free protection for your applications and to get access to all the features.
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