r10k 1.2.4 → 1.3.0rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/{CHANGELOG → CHANGELOG.mkd} +51 -41
- data/doc/dynamic-environments/configuration.mkd +1 -1
- data/doc/dynamic-environments/git-environments.markdown +19 -0
- data/doc/dynamic-environments/usage.mkd +6 -0
- data/lib/r10k/cli/deploy.rb +15 -0
- data/lib/r10k/cli/ext/logging.rb +0 -1
- data/lib/r10k/cli/module/deploy.rb +0 -1
- data/lib/r10k/cli/puppetfile.rb +2 -2
- data/lib/r10k/cli.rb +2 -16
- data/lib/r10k/deployment/environment.rb +9 -79
- data/lib/r10k/deployment/source.rb +15 -89
- data/lib/r10k/deployment.rb +13 -14
- data/lib/r10k/environment/base.rb +42 -0
- data/lib/r10k/environment/git.rb +79 -0
- data/lib/r10k/environment/svn.rb +73 -0
- data/lib/r10k/environment.rb +7 -0
- data/lib/r10k/execution.rb +0 -1
- data/lib/r10k/git/cache.rb +11 -5
- data/lib/r10k/git/repository.rb +1 -8
- data/lib/r10k/git/working_dir.rb +11 -34
- data/lib/r10k/git.rb +0 -1
- data/lib/r10k/instance_cache.rb +32 -0
- data/lib/r10k/keyed_factory.rb +39 -0
- data/lib/r10k/module/forge.rb +2 -3
- data/lib/r10k/module/svn.rb +0 -1
- data/lib/r10k/puppetfile.rb +0 -1
- data/lib/r10k/registry.rb +3 -31
- data/lib/r10k/source/base.rb +60 -0
- data/lib/r10k/source/git.rb +195 -0
- data/lib/r10k/source/svn.rb +140 -0
- data/lib/r10k/source.rb +39 -0
- data/lib/r10k/svn/remote.rb +48 -0
- data/lib/r10k/svn/working_dir.rb +0 -2
- data/lib/r10k/svn.rb +6 -0
- data/lib/r10k/task/deployment.rb +1 -2
- data/lib/r10k/task.rb +0 -2
- data/lib/r10k/task_runner.rb +0 -1
- data/lib/r10k/util/core_ext/hash_ext.rb +19 -0
- data/lib/r10k/util/subprocess.rb +0 -1
- data/lib/r10k/version.rb +1 -1
- data/lib/r10k.rb +1 -0
- data/spec/unit/deployment/environment_spec.rb +16 -15
- data/spec/unit/environment/git_spec.rb +81 -0
- data/spec/unit/environment/svn_spec.rb +76 -0
- data/spec/unit/git/repository_spec.rb +0 -10
- data/spec/unit/git/working_dir_spec.rb +1 -110
- data/spec/unit/{registry_spec.rb → instance_cache_spec.rb} +3 -3
- data/spec/unit/keyed_factory_spec.rb +51 -0
- data/spec/unit/source/git_spec.rb +274 -0
- data/spec/unit/source/svn_spec.rb +102 -0
- data/spec/unit/source_spec.rb +10 -0
- data/spec/unit/svn/remote_spec.rb +21 -0
- data/spec/unit/util/core_ext/hash_ext_spec.rb +63 -0
- metadata +36 -10
- data/lib/r10k/git/alternates.rb +0 -49
- data/spec/unit/git/alternates_spec.rb +0 -90
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'r10k/logging'
|
2
|
+
require 'r10k/puppetfile'
|
3
|
+
require 'r10k/git/working_dir'
|
4
|
+
|
5
|
+
# This class implements an environment based on a Git branch.
|
6
|
+
#
|
7
|
+
# @since 1.3.0
|
8
|
+
class R10K::Environment::Git < R10K::Environment::Base
|
9
|
+
|
10
|
+
include R10K::Logging
|
11
|
+
|
12
|
+
# @!attribute [r] remote
|
13
|
+
# @return [String] The URL to the remote git repository
|
14
|
+
attr_reader :remote
|
15
|
+
|
16
|
+
# @!attribute [r] ref
|
17
|
+
# @return [String] The git reference to use for this environment
|
18
|
+
attr_reader :ref
|
19
|
+
|
20
|
+
# @!attribute [r] working_dir
|
21
|
+
# @api private
|
22
|
+
# @return [R10K::Git::WorkingDir] The git working directory backing this environment
|
23
|
+
attr_reader :working_dir
|
24
|
+
|
25
|
+
# @!attribute [r] puppetfile
|
26
|
+
# @api public
|
27
|
+
# @return [R10K::Puppetfile] The puppetfile instance associated with this environment
|
28
|
+
attr_reader :puppetfile
|
29
|
+
|
30
|
+
# Initialize the given SVN environment.
|
31
|
+
#
|
32
|
+
# @param name [String] The unique name describing this environment.
|
33
|
+
# @param basedir [String] The base directory where this environment will be created.
|
34
|
+
# @param dirname [String] The directory name for this environment.
|
35
|
+
# @param options [Hash] An additional set of options for this environment.
|
36
|
+
#
|
37
|
+
# @param options [String] :remote The URL to the remote git repository
|
38
|
+
# @param options [String] :ref The git reference to use for this environment
|
39
|
+
def initialize(name, basedir, dirname, options = {})
|
40
|
+
super
|
41
|
+
@remote = options[:remote]
|
42
|
+
@ref = options[:ref]
|
43
|
+
|
44
|
+
@working_dir = R10K::Git::WorkingDir.new(@ref, @remote, @basedir, @dirname)
|
45
|
+
@puppetfile = R10K::Puppetfile.new(@full_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clone or update the given Git environment.
|
49
|
+
#
|
50
|
+
# If the environment is being created for the first time, it will
|
51
|
+
# automatically update all modules to ensure that the environment is complete.
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
# @return [void]
|
55
|
+
def sync
|
56
|
+
recursive_needed = !(@working_dir.cloned?)
|
57
|
+
@working_dir.sync
|
58
|
+
|
59
|
+
if recursive_needed
|
60
|
+
logger.debug "Environment #{@full_path} is a fresh clone; automatically updating modules."
|
61
|
+
sync_modules
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def sync_modules
|
67
|
+
modules.each do |mod|
|
68
|
+
logger.debug "Deploying module #{mod.name}"
|
69
|
+
mod.sync
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Array<R10K::Module::Base>] All modules defined in the Puppetfile
|
74
|
+
# associated with this environment.
|
75
|
+
def modules
|
76
|
+
@puppetfile.load
|
77
|
+
@puppetfile.modules
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'r10k/puppetfile'
|
2
|
+
require 'r10k/svn/working_dir'
|
3
|
+
|
4
|
+
# This class implements an environment based on an SVN branch.
|
5
|
+
#
|
6
|
+
# @since 1.3.0
|
7
|
+
class R10K::Environment::SVN < R10K::Environment::Base
|
8
|
+
|
9
|
+
include R10K::Logging
|
10
|
+
|
11
|
+
# @!attribute [r] remote
|
12
|
+
# @return [String] The URL to the remote SVN branch to check out
|
13
|
+
attr_reader :remote
|
14
|
+
|
15
|
+
# @!attribute [r] working_dir
|
16
|
+
# @api private
|
17
|
+
# @return [R10K::SVN::WorkingDir] The SVN working directory backing this environment
|
18
|
+
attr_reader :working_dir
|
19
|
+
|
20
|
+
# @!attribute [r] puppetfile
|
21
|
+
# @api public
|
22
|
+
# @return [R10K::Puppetfile] The puppetfile instance associated with this environment
|
23
|
+
attr_reader :puppetfile
|
24
|
+
|
25
|
+
# Initialize the given SVN environment.
|
26
|
+
#
|
27
|
+
# @param name [String] The unique name describing this environment.
|
28
|
+
# @param basedir [String] The base directory where this environment will be created.
|
29
|
+
# @param dirname [String] The directory name for this environment.
|
30
|
+
# @param options [Hash] An additional set of options for this environment.
|
31
|
+
#
|
32
|
+
# @param options [String] :remote The URL to the remote SVN branch to check out
|
33
|
+
def initialize(name, basedir, dirname, options = {})
|
34
|
+
super
|
35
|
+
|
36
|
+
@remote = options[:remote]
|
37
|
+
|
38
|
+
@working_dir = R10K::SVN::WorkingDir.new(Pathname.new(@full_path))
|
39
|
+
@puppetfile = R10K::Puppetfile.new(@full_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Perform an initial checkout of the SVN repository or update the repository.
|
43
|
+
#
|
44
|
+
# If the environment is being created for the first time, it will
|
45
|
+
# automatically update all modules to ensure that the environment is complete.
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
# @return [void]
|
49
|
+
def sync
|
50
|
+
if @working_dir.is_svn?
|
51
|
+
@working_dir.update
|
52
|
+
else
|
53
|
+
@working_dir.checkout(@remote)
|
54
|
+
logger.debug "Environment #{@full_path} is a fresh clone; automatically updating modules."
|
55
|
+
sync_modules
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Array<R10K::Module::Base>] All modules defined in the Puppetfile
|
60
|
+
# associated with this environment.
|
61
|
+
def modules
|
62
|
+
@puppetfile.load
|
63
|
+
@puppetfile.modules
|
64
|
+
end
|
65
|
+
|
66
|
+
# @api private
|
67
|
+
def sync_modules
|
68
|
+
modules.each do |mod|
|
69
|
+
logger.debug "Deploying module #{mod.name}"
|
70
|
+
mod.sync
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/r10k/execution.rb
CHANGED
data/lib/r10k/git/cache.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'r10k/logging'
|
2
|
-
|
3
1
|
require 'r10k/git'
|
4
2
|
require 'r10k/git/repository'
|
5
3
|
|
@@ -15,12 +13,20 @@ class R10K::Git::Cache < R10K::Git::Repository
|
|
15
13
|
|
16
14
|
def_setting_attr :cache_root, File.expand_path(ENV['HOME'] ? '~/.r10k/git': '/root/.r10k/git')
|
17
15
|
|
18
|
-
|
19
|
-
|
16
|
+
# Lazily construct an instance cache for R10K::Git::Cache objects
|
17
|
+
# @api private
|
18
|
+
def self.instance_cache
|
19
|
+
@instance_cache ||= R10K::InstanceCache.new(self)
|
20
20
|
end
|
21
21
|
|
22
|
+
# Generate a new instance with the given remote or return an existing object
|
23
|
+
# with the given remote. This should be used over R10K::Git::Cache.new.
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
# @param remote [String] The git remote to cache
|
27
|
+
# @return [R10K::Git::Cache] The requested cache object.
|
22
28
|
def self.generate(remote)
|
23
|
-
|
29
|
+
instance_cache.generate(remote)
|
24
30
|
end
|
25
31
|
|
26
32
|
include R10K::Logging
|
data/lib/r10k/git/repository.rb
CHANGED
@@ -84,7 +84,7 @@ class R10K::Git::Repository
|
|
84
84
|
|
85
85
|
ret = {}
|
86
86
|
output.stdout.each_line do |line|
|
87
|
-
next if line.match
|
87
|
+
next if line.match /\(push\)/
|
88
88
|
name, url, _ = line.split(/\s+/)
|
89
89
|
ret[name] = url
|
90
90
|
end
|
@@ -92,13 +92,6 @@ class R10K::Git::Repository
|
|
92
92
|
ret
|
93
93
|
end
|
94
94
|
|
95
|
-
def tags
|
96
|
-
entries = []
|
97
|
-
output = git(['tag', '-l'], :git_dir => @git_dir).stdout
|
98
|
-
output.each_line { |line| entries << line.chomp }
|
99
|
-
entries
|
100
|
-
end
|
101
|
-
|
102
95
|
private
|
103
96
|
|
104
97
|
# Fetch objects and refs from the given git remote
|
data/lib/r10k/git/working_dir.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'forwardable'
|
2
|
-
require 'r10k/logging'
|
3
2
|
require 'r10k/git'
|
4
3
|
require 'r10k/git/cache'
|
5
4
|
|
@@ -41,8 +40,7 @@ class R10K::Git::WorkingDir < R10K::Git::Repository
|
|
41
40
|
@full_path = File.join(@basedir, @dirname)
|
42
41
|
@git_dir = File.join(@full_path, '.git')
|
43
42
|
|
44
|
-
@
|
45
|
-
@cache = R10K::Git::Cache.generate(@remote)
|
43
|
+
@cache = R10K::Git::Cache.generate(@remote)
|
46
44
|
|
47
45
|
if ref.is_a? String
|
48
46
|
@ref = R10K::Git::Ref.new(ref, self)
|
@@ -62,9 +60,7 @@ class R10K::Git::WorkingDir < R10K::Git::Repository
|
|
62
60
|
end
|
63
61
|
|
64
62
|
def update
|
65
|
-
|
66
|
-
|
67
|
-
if ref_needs_fetch?
|
63
|
+
if fetch?
|
68
64
|
fetch_from_cache
|
69
65
|
checkout(@ref)
|
70
66
|
elsif needs_checkout?
|
@@ -117,17 +113,22 @@ class R10K::Git::WorkingDir < R10K::Git::Repository
|
|
117
113
|
|
118
114
|
private
|
119
115
|
|
120
|
-
|
121
|
-
# @return [true, false]
|
122
|
-
def ref_needs_fetch?
|
116
|
+
def fetch?
|
123
117
|
@ref.fetch?
|
124
118
|
end
|
125
119
|
|
126
120
|
def fetch_from_cache
|
121
|
+
set_cache_remote
|
127
122
|
@cache.sync
|
128
123
|
fetch('cache')
|
129
124
|
end
|
130
125
|
|
126
|
+
def set_cache_remote
|
127
|
+
if self.remote != @cache.remote
|
128
|
+
git ["remote", "set-url", "cache", @cache.git_dir], :path => @full_path
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
131
132
|
# Perform a non-bare clone of a git repository.
|
132
133
|
def clone
|
133
134
|
@cache.sync
|
@@ -149,30 +150,6 @@ class R10K::Git::WorkingDir < R10K::Git::Repository
|
|
149
150
|
expected = ref.sha1
|
150
151
|
actual = rev_parse('HEAD')
|
151
152
|
|
152
|
-
!(expected == actual)
|
153
|
-
end
|
154
|
-
|
155
|
-
def update_remotes?
|
156
|
-
real_remotes = remotes
|
157
|
-
|
158
|
-
expected_origin = @remote
|
159
|
-
expected_cache = @cache.git_dir
|
160
|
-
|
161
|
-
!(expected_origin == real_remotes['origin'] and expected_cache == real_remotes['cache'])
|
162
|
-
end
|
163
|
-
|
164
|
-
def update_remotes
|
165
|
-
# todo: remove all existing refs as they may belong to the old remote
|
166
|
-
git ['remote', 'set-url', 'origin', remote], :path => @full_path
|
167
|
-
git ['remote', 'set-url', 'cache', @cache.git_dir], :path => @full_path
|
168
|
-
@alternates << File.join(@cache.git_dir, 'objects')
|
169
|
-
logger.debug("Removing stale git tags from #{@full_path}")
|
170
|
-
remove_tags
|
171
|
-
end
|
172
|
-
|
173
|
-
def remove_tags
|
174
|
-
tags.each do |tag|
|
175
|
-
git ['tag', '-d', tag], :path => @full_path
|
176
|
-
end
|
153
|
+
! (expected == actual)
|
177
154
|
end
|
178
155
|
end
|
data/lib/r10k/git.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
module R10K
|
2
|
+
|
3
|
+
# This class implements a generic object memoization container. It caches
|
4
|
+
# new objects and returns cached objects based on the instantiation arguments.
|
5
|
+
class InstanceCache
|
6
|
+
|
7
|
+
# Initialize a new registry with a given class
|
8
|
+
#
|
9
|
+
# @param klass [Class] The class to memoize
|
10
|
+
# @param method [Symbol] The method name to use when creating objects.
|
11
|
+
# Defaults to :new.
|
12
|
+
def initialize(klass, method = :new)
|
13
|
+
@klass = klass
|
14
|
+
@method = method
|
15
|
+
@instances = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new object, or return a memoized object.
|
19
|
+
#
|
20
|
+
# @param args [*Object] The arguments to pass to the initialize method
|
21
|
+
#
|
22
|
+
# @return [Object] A memoized instance of the registered class
|
23
|
+
def generate(*args)
|
24
|
+
@instances[args] ||= @klass.send(@method, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Clear all memoized objects
|
28
|
+
def clear!
|
29
|
+
@instances = {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module R10K
|
2
|
+
|
3
|
+
# This implements a factory by storing classes indexed with a given key and
|
4
|
+
# creates objects based on that key.
|
5
|
+
class KeyedFactory
|
6
|
+
|
7
|
+
# @!attribute [r] implementations
|
8
|
+
# @return [Hash<Object, Class>] A hash of keys and the associated
|
9
|
+
# implementations that this factory can generate.
|
10
|
+
attr_reader :implementations
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@implementations = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def register(key, klass)
|
17
|
+
if @implementations.has_key?(key)
|
18
|
+
raise DuplicateImplementationError, "Class already registered for #{key}"
|
19
|
+
else
|
20
|
+
@implementations[key] = klass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def retrieve(key)
|
25
|
+
@implementations[key]
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate(key, *args)
|
29
|
+
if (impl = @implementations[key])
|
30
|
+
impl.new(*args)
|
31
|
+
else
|
32
|
+
raise UnknownImplementationError, "No class registered for #{key}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class DuplicateImplementationError < StandardError; end
|
37
|
+
class UnknownImplementationError < StandardError; end
|
38
|
+
end
|
39
|
+
end
|
data/lib/r10k/module/forge.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'r10k/module'
|
2
2
|
require 'r10k/errors'
|
3
|
-
require 'r10k/logging'
|
4
3
|
require 'r10k/module/metadata'
|
5
4
|
require 'r10k/util/subprocess'
|
6
5
|
require 'r10k/module_repository/forge'
|
@@ -126,7 +125,7 @@ class R10K::Module::Forge < R10K::Module::Base
|
|
126
125
|
cmd = []
|
127
126
|
cmd << 'install'
|
128
127
|
cmd << "--version=#{expected_version}" if expected_version
|
129
|
-
cmd << "--
|
128
|
+
cmd << "--ignore-dependencies"
|
130
129
|
cmd << @full_name
|
131
130
|
pmt cmd
|
132
131
|
end
|
@@ -135,7 +134,7 @@ class R10K::Module::Forge < R10K::Module::Base
|
|
135
134
|
cmd = []
|
136
135
|
cmd << 'upgrade'
|
137
136
|
cmd << "--version=#{expected_version}" if expected_version
|
138
|
-
cmd << "--
|
137
|
+
cmd << "--ignore-dependencies"
|
139
138
|
cmd << @full_name
|
140
139
|
pmt cmd
|
141
140
|
end
|
data/lib/r10k/module/svn.rb
CHANGED
data/lib/r10k/puppetfile.rb
CHANGED
data/lib/r10k/registry.rb
CHANGED
@@ -1,32 +1,4 @@
|
|
1
|
-
|
1
|
+
require 'r10k/instance_cache'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
class Registry
|
6
|
-
|
7
|
-
# Initialize a new registry with a given class
|
8
|
-
#
|
9
|
-
# @param klass [Class] The class to memoize
|
10
|
-
# @param method [Symbol] The method name to use when creating objects.
|
11
|
-
# Defaults to :new.
|
12
|
-
def initialize(klass, method = :new)
|
13
|
-
@klass = klass
|
14
|
-
@method = method
|
15
|
-
@instances = {}
|
16
|
-
end
|
17
|
-
|
18
|
-
# Create a new object, or return a memoized object.
|
19
|
-
#
|
20
|
-
# @param args [*Object] The arguments to pass to the initialize method
|
21
|
-
#
|
22
|
-
# @return [Object] A memoized instance of the registered class
|
23
|
-
def generate(*args)
|
24
|
-
@instances[args] ||= @klass.send(@method, *args)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Clear all memoized objects
|
28
|
-
def clear!
|
29
|
-
@instances = {}
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
3
|
+
# @deprecated
|
4
|
+
R10K::Registry = R10K::InstanceCache
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# This class defines a common interface for source implementations.
|
2
|
+
#
|
3
|
+
# @since 1.3.0
|
4
|
+
class R10K::Source::Base
|
5
|
+
|
6
|
+
# @!attribute [r] basedir
|
7
|
+
# @return [String] The path this source will place environments in
|
8
|
+
attr_reader :basedir
|
9
|
+
|
10
|
+
# @!attribute [r] name
|
11
|
+
# @return [String] The short name for this environment source
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @!attribute [r] prefix
|
15
|
+
# @return [true, false] Whether the source name should be prefixed to each
|
16
|
+
# environment basedir. Defaults to false
|
17
|
+
attr_reader :prefix
|
18
|
+
|
19
|
+
# Initialize the given source.
|
20
|
+
#
|
21
|
+
# @param name [String] The identifier for this source.
|
22
|
+
# @param basedir [String] The base directory where the generated environments will be created.
|
23
|
+
# @param options [Hash] An additional set of options for this source. The
|
24
|
+
# semantics of this hash may depend on the source implementation.
|
25
|
+
#
|
26
|
+
# @option options [Boolean] :prefix Whether to prefix the source name to the
|
27
|
+
# environment directory names. All sources should respect this option.
|
28
|
+
# Defaults to false.
|
29
|
+
def initialize(name, basedir, options = {})
|
30
|
+
@name = name
|
31
|
+
@basedir = basedir
|
32
|
+
@prefix = options.delete(:prefix)
|
33
|
+
@options = options
|
34
|
+
end
|
35
|
+
|
36
|
+
# Perform any actions needed for loading environments that may have side
|
37
|
+
# effects.
|
38
|
+
#
|
39
|
+
# Actions done during preloading may include things like updating caches or
|
40
|
+
# performing network queries. If an environment has not been preloaded but
|
41
|
+
# {#environments} is invoked, it should return the best known state of
|
42
|
+
# environments or return an empty list.
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
# @abstract
|
46
|
+
# @return [void]
|
47
|
+
def preload!
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# Enumerate the environments associated with this SVN source.
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
# @abstract
|
55
|
+
# @return [Array<R10K::Environment::Base>] An array of environments created
|
56
|
+
# from this source.
|
57
|
+
def environments
|
58
|
+
raise NotImplementedError, "#{self.class} has not implemented method #{__method__}"
|
59
|
+
end
|
60
|
+
end
|