vanagon 0.15.38 → 0.18.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -23
  3. data/bin/build +4 -25
  4. data/bin/build_host_info +4 -17
  5. data/bin/build_requirements +4 -31
  6. data/bin/inspect +4 -21
  7. data/bin/render +4 -22
  8. data/bin/ship +4 -28
  9. data/bin/sign +4 -11
  10. data/bin/vanagon +7 -0
  11. data/extras/completions/vanagon.bash +38 -0
  12. data/extras/completions/vanagon.zsh +41 -0
  13. data/lib/vanagon.rb +1 -1
  14. data/lib/vanagon/cli.rb +102 -0
  15. data/lib/vanagon/cli/build.rb +83 -0
  16. data/lib/vanagon/cli/build_host_info.rb +57 -0
  17. data/lib/vanagon/cli/build_requirements.rb +68 -0
  18. data/lib/vanagon/cli/completion.rb +43 -0
  19. data/lib/vanagon/cli/inspect.rb +73 -0
  20. data/lib/vanagon/cli/list.rb +75 -0
  21. data/lib/vanagon/cli/render.rb +59 -0
  22. data/lib/vanagon/cli/ship.rb +52 -0
  23. data/lib/vanagon/cli/sign.rb +34 -0
  24. data/lib/vanagon/driver.rb +35 -27
  25. data/lib/vanagon/engine/always_be_scheduling.rb +271 -1
  26. data/lib/vanagon/engine/docker.rb +101 -14
  27. data/lib/vanagon/engine/pooler.rb +7 -3
  28. data/lib/vanagon/platform.rb +3 -0
  29. data/lib/vanagon/platform/deb.rb +2 -0
  30. data/lib/vanagon/platform/dsl.rb +11 -0
  31. data/lib/vanagon/utilities.rb +30 -8
  32. data/resources/osx/postinstall.erb +1 -1
  33. data/resources/solaris/10/postinstall.erb +1 -1
  34. data/spec/lib/vanagon/cli_spec.rb +226 -0
  35. data/spec/lib/vanagon/driver_spec.rb +1 -1
  36. data/spec/lib/vanagon/engine/always_be_scheduling_spec.rb +113 -1
  37. data/spec/lib/vanagon/engine/docker_spec.rb +74 -16
  38. data/spec/lib/vanagon/engine/ec2_spec.rb +2 -0
  39. data/spec/lib/vanagon/engine/pooler_spec.rb +1 -1
  40. data/spec/spec_helper.rb +1 -0
  41. metadata +61 -34
  42. data/lib/vanagon/optparse.rb +0 -86
  43. data/spec/lib/vanagon/optparse_spec.rb +0 -64
@@ -10,6 +10,7 @@ class Vanagon
10
10
 
11
11
  @docker_cmd = Vanagon::Utilities.find_program_on_path('docker')
12
12
  @required_attributes << "docker_image"
13
+ @required_attributes.delete('ssh_port') if @platform.use_docker_exec
13
14
  end
14
15
 
15
16
  # Get the engine name
@@ -33,25 +34,16 @@ class Vanagon
33
34
  # This method is used to obtain a vm to build upon using
34
35
  # a docker container.
35
36
  # @raise [Vanagon::Error] if a target cannot be obtained
36
- def select_target # rubocop:disable Metrics/AbcSize
37
+ def select_target
38
+ ssh_args = @platform.use_docker_exec ? '' : "-p #{@platform.ssh_port}:22"
37
39
  extra_args = @platform.docker_run_args.nil? ? [] : @platform.docker_run_args
38
40
 
39
- Vanagon::Utilities.ex("#{@docker_cmd} run -d --name #{build_host_name}-builder -p #{@platform.ssh_port}:22 #{extra_args.join(' ')} #{@platform.docker_image}")
41
+ Vanagon::Utilities.ex("#{@docker_cmd} run -d --name #{build_host_name}-builder #{ssh_args} #{extra_args.join(' ')} #{@platform.docker_image}")
40
42
  @target = 'localhost'
41
43
 
42
- # Wait for ssh to come up in the container. Retry 5 times with a 1
43
- # second sleep between errors to account for network resets while SSHD
44
- # is starting. Allow a maximum of 5 seconds for SSHD to start.
45
- Vanagon::Utilities.retry_with_timeout(5, 5) do
46
- begin
47
- Vanagon::Utilities.remote_ssh_command("#{@target_user}@#{@target}", 'exit', @platform.ssh_port)
48
- rescue StandardError => e
49
- sleep(1) # Give SSHD some time to start.
50
- raise e
51
- end
52
- end
44
+ wait_for_ssh unless @platform.use_docker_exec
53
45
  rescue StandardError => e
54
- raise Vanagon::Error.wrap(e, "Something went wrong getting a target vm to build on using docker. Ssh was not up in the container after 5 seconds.")
46
+ raise Vanagon::Error.wrap(e, "Something went wrong getting a target vm to build on using Docker.")
55
47
  end
56
48
 
57
49
  # This method is used to tell the vmpooler to delete the instance of the
@@ -62,6 +54,101 @@ class Vanagon
62
54
  rescue Vanagon::Error => e
63
55
  warn "There was a problem tearing down the docker container #{build_host_name}-builder (#{e.message})."
64
56
  end
57
+
58
+ def dispatch(command, return_output = false)
59
+ if @platform.use_docker_exec
60
+ docker_exec(command, return_output)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def ship_workdir(workdir)
67
+ if @platform.use_docker_exec
68
+ docker_cp_globs_to("#{workdir}/*", @remote_workdir)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ def retrieve_built_artifact(artifacts_to_fetch, no_packaging)
75
+ if @platform.use_docker_exec
76
+ output_path = 'output/'
77
+ FileUtils.mkdir_p(output_path)
78
+ unless no_packaging
79
+ artifacts_to_fetch << "#{@remote_workdir}/output/*"
80
+ end
81
+
82
+ docker_cp_globs_from(artifacts_to_fetch, 'output/')
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ # Execute a command on a container via docker exec
89
+ def docker_exec(command, return_output = false)
90
+ command = command.gsub("'", "'\\\\''")
91
+ Vanagon::Utilities.local_command("#{@docker_cmd} exec #{build_host_name}-builder /bin/sh -c '#{command}'",
92
+ return_command_output: return_output)
93
+ end
94
+
95
+ # Copy files between a container and the host
96
+ def docker_cp(source, target)
97
+ Vanagon::Utilities.ex("#{@docker_cmd} cp '#{source}' '#{target}'")
98
+ end
99
+
100
+ # Copy files matching a glob pattern from the host to the container
101
+ def docker_cp_globs_to(globs, container_path)
102
+ Array(globs).each do |glob|
103
+ Dir.glob(glob).each do |path|
104
+ docker_cp(path, "#{build_host_name}-builder:#{container_path}")
105
+ end
106
+ end
107
+ end
108
+
109
+ # Copy files matching a glob pattern from the container to the host
110
+ #
111
+ # @note Globs are expanded by running `/bin/sh` in the container, which
112
+ # may not support the same variety of expressions as Ruby's `Dir.glob`.
113
+ # For example, `**` may not work.
114
+ def docker_cp_globs_from(globs, host_path)
115
+ Array(globs).each do |glob|
116
+ # Match the behavior of `rsync -r` when both paths are directories
117
+ # by copying the contents of the directory instead of the directory.
118
+ glob += '*' if glob.end_with?('/') && host_path.end_with?('/')
119
+
120
+ # TODO: This doesn't handle "interesting" paths. E.g. paths with
121
+ # spaces or other special non-glob characters. This could be
122
+ # fixed with a variant of `Shellwords.shellescape` that allows
123
+ # glob characters to pass through.
124
+ paths = docker_exec(%(for file in #{glob};do [ -e "$file" ] && printf '%s\\0' "${file}";done), true).split("\0")
125
+
126
+ paths.each do |path|
127
+ docker_cp("#{build_host_name}-builder:#{path}", host_path)
128
+ end
129
+ end
130
+ end
131
+
132
+ # Wait for ssh to come up in the container
133
+ #
134
+ # Retry 5 times with a 1 second sleep between errors to account for
135
+ # network resets while SSHD is starting. Allow a maximum of 5 seconds for
136
+ # SSHD to start.
137
+ #
138
+ # @raise [Vanagon::Error] if a SSH connection cannot be established.
139
+ # @return [void]
140
+ def wait_for_ssh
141
+ Vanagon::Utilities.retry_with_timeout(5, 5) do
142
+ begin
143
+ Vanagon::Utilities.remote_ssh_command("#{@target_user}@#{@target}", 'exit', @platform.ssh_port)
144
+ rescue StandardError => e
145
+ sleep(1) # Give SSHD some time to start.
146
+ raise e
147
+ end
148
+ end
149
+ rescue StandardError => e
150
+ raise Vanagon::Error.wrap(e, "SSH was not up in the container after 5 seconds.")
151
+ end
65
152
  end
66
153
  end
67
154
  end
@@ -1,6 +1,10 @@
1
1
  require 'vanagon/engine/base'
2
2
  require 'yaml'
3
3
 
4
+ ### Note this class is deprecated in favor of using the ABS Engine. The pooler has changed it's API with regards to
5
+ # getting VMs for ondemand provisioning and would need to be updated here.
6
+ # See DIO-1066
7
+
4
8
  class Vanagon
5
9
  class Engine
6
10
  class Pooler < Base
@@ -10,7 +14,7 @@ class Vanagon
10
14
  def initialize(platform, target = nil, **opts)
11
15
  super
12
16
 
13
- @available_poolers = ["http://vmpooler.delivery.puppetlabs.net", "https://nspooler-service-prod-1.delivery.puppetlabs.net"]
17
+ @available_poolers = ["https://vmpooler.delivery.puppetlabs.net", "https://nspooler-service-prod-1.delivery.puppetlabs.net"]
14
18
  @token = load_token
15
19
  @required_attributes << "vmpooler_template"
16
20
  end
@@ -63,7 +67,7 @@ class Vanagon
63
67
 
64
68
  # Read a vmpooler token from the yaml formatted vmfloaty config,
65
69
  # as outlined by the vmfloaty project:
66
- # https://github.com/briancain/vmfloaty
70
+ # https://github.com/puppetlabs/vmfloaty
67
71
  #
68
72
  # @return [String, nil] the vmfloaty vmpooler token value
69
73
  def read_vmfloaty_token(path = "~/.vmfloaty.yml")
@@ -75,7 +79,7 @@ class Vanagon
75
79
  end
76
80
  private :read_vmfloaty_token
77
81
 
78
- # This method is used to obtain a vm to build upon using the Puppet Labs'
82
+ # This method is used to obtain a vm to build upon using Puppet's internal
79
83
  # vmpooler (https://github.com/puppetlabs/vmpooler) or other pooler technologies
80
84
  # leveraging the same API
81
85
  # @raise [Vanagon::Error] if a target cannot be obtained
@@ -115,6 +115,7 @@ class Vanagon
115
115
  # Docker engine specific
116
116
  attr_accessor :docker_image
117
117
  attr_accessor :docker_run_args
118
+ attr_accessor :use_docker_exec
118
119
 
119
120
  # AWS engine specific
120
121
  attr_accessor :aws_ami
@@ -238,6 +239,8 @@ class Vanagon
238
239
  @copy ||= "cp"
239
240
  @shasum ||= "sha1sum"
240
241
 
242
+ @use_docker_exec = false
243
+
241
244
  # Our first attempt at defining metadata about a platform
242
245
  @cross_compiled ||= false
243
246
  @valid_operators ||= ['<', '>', '<=', '>=', '=']
@@ -13,6 +13,8 @@ class Vanagon
13
13
  copy_extensions = '*.deb'
14
14
  end
15
15
  pkg_arch_opt = project.noarch ? "" : "-a#{@architecture}"
16
+ pkg_arch_opt = '-aarm64' if pkg_arch_opt == '-aaarch64'
17
+
16
18
  ["mkdir -p output/#{target_dir}",
17
19
  "mkdir -p $(tempdir)/#{project.name}-#{project.version}",
18
20
  "cp #{project.name}-#{project.version}.tar.gz $(tempdir)/#{project.name}_#{project.version}.orig.tar.gz",
@@ -284,6 +284,17 @@ class Vanagon
284
284
  @platform.docker_run_args = Array(args)
285
285
  end
286
286
 
287
+ # Specify whether to use Docker exec instead of SSH to run commands
288
+ #
289
+ # This also causes Vanagon to use `docker cp` instead of `rsync` when
290
+ # copying files.
291
+ #
292
+ # @param bool [Boolean] a boolean value indicating whether to use
293
+ # `docker exec` and `docker cp` over `ssh` and `rsync`.
294
+ def use_docker_exec(bool)
295
+ @platform.use_docker_exec = bool
296
+ end
297
+
287
298
  # Set the ami for the platform to use
288
299
  #
289
300
  # @param ami [String] the ami id used.
@@ -39,17 +39,16 @@ class Vanagon
39
39
  end
40
40
 
41
41
  # Simple wrapper around Net::HTTP. Will make a request of the given type to
42
- # the given url and return the body as parsed by JSON.
42
+ # the given url and return the response object
43
43
  #
44
44
  # @param url [String] The url to make the request against (needs to be parsable by URI
45
45
  # @param type [String] One of the supported request types (currently 'get', 'post', 'delete')
46
46
  # @param payload [String] The request body data payload used for POST and PUT
47
47
  # @param header [Hash] Send additional information in the HTTP request header
48
- # @return [Hash] The response body is parsed by JSON and returned
48
+ # @return [Net::HTTPAccepted] The response object
49
49
  # @raise [RuntimeError, Vanagon::Error] an exception is raised if the
50
- # action is not supported, or if there is a problem with the http request,
51
- # or if the response is not JSON
52
- def http_request(url, type, payload = {}.to_json, header = nil) # rubocop:disable Metrics/AbcSize
50
+ # action is not supported, or if there is a problem with the http request
51
+ def http_request_generic(url, type, payload = {}.to_json, header = nil) # rubocop:disable Metrics/AbcSize
53
52
  uri = URI.parse(url)
54
53
  http = Net::HTTP.new(uri.host, uri.port)
55
54
  http.use_ssl = true if uri.scheme == 'https'
@@ -77,14 +76,37 @@ class Vanagon
77
76
  end
78
77
 
79
78
  response = http.request(request)
80
-
81
- JSON.parse(response.body)
79
+ response
82
80
  rescue Errno::ETIMEDOUT, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
83
81
  EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
84
82
  Net::ProtocolError => e
85
83
  raise Vanagon::Error.wrap(e, "Problem reaching #{url}. Is #{uri.host} down?")
84
+ end
85
+
86
+ # uses http_request_generic and returns the body as parsed by JSON.
87
+ # @param url [String] The url to make the request against (needs to be parsable by URI
88
+ # @param type [String] One of the supported request types (currently 'get', 'post', 'delete')
89
+ # @param payload [String] The request body data payload used for POST and PUT
90
+ # @param header [Hash] Send additional information in the HTTP request header
91
+ # @return [Hash] The response in JSON format
92
+ # @raise [RuntimeError, Vanagon::Error] an exception is raised if the response
93
+ # body cannot be parsed as JSON
94
+ def http_request(url, type, payload = {}.to_json, header = nil)
95
+ response = http_request_generic(url, type, payload, header)
96
+ JSON.parse(response.body)
86
97
  rescue JSON::ParserError => e
87
- raise Vanagon::Error.wrap(e, "#{uri.host} handed us a response that doesn't look like JSON.")
98
+ raise Vanagon::Error.wrap(e, "#{url} handed us a response that doesn't look like JSON.")
99
+ end
100
+
101
+ # uses http_request_generic and returns the response code.
102
+ # @param url [String] The url to make the request against (needs to be parsable by URI
103
+ # @param type [String] One of the supported request types (currently 'get', 'post', 'delete')
104
+ # @param payload [String] The request body data payload used for POST and PUT
105
+ # @param header [Hash] Send additional information in the HTTP request header
106
+ # @return [String] The response code eg 202, 200 etc
107
+ def http_request_code(url, type, payload = {}.to_json, header = nil)
108
+ response = http_request_generic(url, type, payload, header)
109
+ response.code
88
110
  end
89
111
 
90
112
  # Similar to rake's sh, the passed command will be executed and an
@@ -5,7 +5,7 @@
5
5
  <%- get_configfiles.each do |config|
6
6
  dest_file = config.path.gsub(/\.pristine$/, '') -%>
7
7
 
8
- if [ -f "<%= dest_file %>" ]; then
8
+ if [ -f "<%= dest_file %>" ] && ! diff "<%= config.path %>" "<%= dest_file %>" > /dev/null; then
9
9
  echo "Detected file at '<%= dest_file %>'; updated file at '<%= config.path %>'."
10
10
  else
11
11
  mv '<%= config.path %>' '<%= dest_file %>'
@@ -9,7 +9,7 @@
9
9
  <%- get_configfiles.each do |config|
10
10
  dest_file = config.path.gsub(/\.pristine$/, '') -%>
11
11
 
12
- if [ -f "<%= dest_file %>" ]; then
12
+ if [ -f "<%= dest_file %>" ] && ! diff "<%= config.path %>" "<%= dest_file %>" > /dev/null; then
13
13
  echo "Detected file at '<%= dest_file %>'; updated file at '<%= config.path %>'."
14
14
  else
15
15
  cp -pr '<%= config.path %>' '<%= dest_file %>'
@@ -0,0 +1,226 @@
1
+ require 'vanagon/cli'
2
+
3
+ ##
4
+ ## Ignore the CLI calling 'exit'
5
+ ##
6
+ RSpec.configure do |rspec|
7
+ rspec.around(:example) do |ex|
8
+ begin
9
+ ex.run
10
+ rescue SystemExit => e
11
+ puts "Got SystemExit: #{e.inspect}. Ignoring"
12
+ end
13
+ end
14
+ end
15
+
16
+ describe Vanagon::CLI do
17
+ describe "options that don't take a value" do
18
+ [:skipcheck, :verbose].each do |flag|
19
+ it "can create an option parser that accepts the #{flag} flag" do
20
+ subject = described_class.new
21
+ expect(subject.parse(%W[build --#{flag} project platform])).to have_key(flag)
22
+ end
23
+ end
24
+
25
+ describe "short options" do
26
+ [["v", :verbose]].each do |short, long|
27
+ it "maps the short option #{short} to #{long}" do
28
+ subject = described_class.new
29
+ expect(subject.parse(%W[build -#{short} project platform])).to include(long => true)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "options that only allow limited values" do
36
+ [[:preserve, ["always", "never", "on-failure"]]].each do |option, values|
37
+ values.each do |value|
38
+ it "can create a parser that accepts \"--#{option} #{value}\"" do
39
+ subject = described_class.new
40
+ expect(subject.parse(%W[build --#{option} #{value} project platform]))
41
+ .to include(option => value.to_sym)
42
+ end
43
+ end
44
+ end
45
+ [[:preserve, ["bad-argument"]]].each do |option, values|
46
+ values.each do |value|
47
+ it "rejects the bad argument \"--#{option} #{value}\"" do
48
+ subject = described_class.new
49
+ expect{subject.parse(%W[build --#{option} #{value} project platform])}
50
+ .to raise_error(Vanagon::InvalidArgument)
51
+ end
52
+ end
53
+ end
54
+ it "preserve defaults to :on-failure" do
55
+ subject = described_class.new
56
+ expect(subject.parse([])).to include(:preserve => :'on-failure')
57
+ end
58
+ end
59
+
60
+
61
+ describe "options that take a value" do
62
+ [:workdir, :configdir, :engine].each do |option|
63
+ it "can create an option parser that accepts the #{option} option" do
64
+ subject = described_class.new
65
+ expect(subject.parse(%W[build --#{option} hello project platform]))
66
+ .to include(option => "hello")
67
+ end
68
+ end
69
+
70
+ describe "short options" do
71
+ [["w", :workdir], ["c", :configdir], ["e", :engine]].each do |short, long|
72
+ it "maps the short option #{short} to #{long}" do
73
+ subject = described_class.new
74
+ expect(subject.parse(%W[build -#{short} hello project platform]))
75
+ .to include(long => "hello")
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe Vanagon::CLI::List do
83
+ let(:cli) { Vanagon::CLI::List.new }
84
+
85
+ describe "#output" do
86
+ let(:list) { ['a', 'b', 'c']}
87
+ it "returns an array if space is false" do
88
+ expect(cli.output(list, false)).to eq(list)
89
+ end
90
+ it "returns space separated if space is true" do
91
+ expect(cli.output(list, true)).to eq('a b c')
92
+ end
93
+ end
94
+
95
+ describe "#run" do
96
+ let(:projects){ ['foo', 'bar', 'baz'] }
97
+ let(:platforms){ ['1', '2', '3'] }
98
+ let(:output_both){
99
+ "- Projects
100
+ foo
101
+ bar
102
+ baz
103
+
104
+ - Platforms
105
+ 1
106
+ 2
107
+ 3
108
+ "
109
+ }
110
+ context "specs with standard config path" do
111
+ before(:each) do
112
+ expect(Dir).to receive(:exist?)
113
+ .with("#{File.join(Dir.pwd, 'configs', 'platforms')}")
114
+ .and_return(true)
115
+ expect(Dir).to receive(:exist?)
116
+ .with("#{File.join(Dir.pwd, 'configs', 'projects')}")
117
+ .and_return(true)
118
+ expect(Dir).to receive(:children)
119
+ .with("#{File.join(Dir.pwd, 'configs', 'projects')}")
120
+ .and_return(projects)
121
+ expect(Dir).to receive(:children)
122
+ .with("#{File.join(Dir.pwd, 'configs', 'platforms')}")
123
+ .and_return(platforms)
124
+ end
125
+ let(:options_empty) { {
126
+ nil=>false,
127
+ :configdir=>"#{Dir.pwd}/configs",
128
+ :platforms=>false,
129
+ :projects=>false,
130
+ :use_spaces=>false
131
+ } }
132
+ let(:options_platforms_only) { {
133
+ nil=>false,
134
+ :configdir=>"#{Dir.pwd}/configs",
135
+ :platforms=>true,
136
+ :projects=>false,
137
+ :use_spaces=>false
138
+ } }
139
+ let(:options_projects_only) { {
140
+ nil=>false,
141
+ :configdir=>"#{Dir.pwd}/configs",
142
+ :platforms=>false,
143
+ :projects=>true,
144
+ :use_spaces=>false
145
+ } }
146
+ let(:options_space_only) { {
147
+ nil=>false,
148
+ :configdir=>"#{Dir.pwd}/configs",
149
+ :platforms=>false,
150
+ :projects=>false,
151
+ :use_spaces=>true
152
+ } }
153
+
154
+ it "outputs projects and platforms with no options passed" do
155
+ expect do
156
+ cli.run(options_empty)
157
+ end.to output(output_both).to_stdout
158
+ end
159
+
160
+ let(:output_both_space){
161
+ "- Projects
162
+ foo bar baz
163
+
164
+ - Platforms
165
+ 1 2 3
166
+ "
167
+ }
168
+ it "outputs projects and platforms space separated" do
169
+ expect do
170
+ cli.run(options_space_only)
171
+ end.to output(output_both_space).to_stdout
172
+ end
173
+
174
+ let(:output_platforms){
175
+ "- Platforms
176
+ 1
177
+ 2
178
+ 3
179
+ "
180
+ }
181
+ it "outputs only platforms when platforms is passed" do
182
+ expect do
183
+ cli.run(options_platforms_only)
184
+ end.to output(output_platforms).to_stdout
185
+ end
186
+
187
+ let(:output_projects){
188
+ "- Projects
189
+ foo
190
+ bar
191
+ baz
192
+ "
193
+ }
194
+ it "outputs only projects when projects is passed" do
195
+ expect do
196
+ cli.run(options_projects_only)
197
+ end.to output(output_projects).to_stdout
198
+ end
199
+ end
200
+
201
+ context "spec with a configdir specified" do
202
+ let(:options_configdir) { {
203
+ nil=>false,
204
+ :configdir=> '/configs',
205
+ :platforms=>false,
206
+ :projects=>false,
207
+ :use_spaces=>false} }
208
+ it "it successfully takes the configs directory" do
209
+ expect(Dir).to receive(:exist?).with('/configs' + '/platforms')
210
+ .and_return(true)
211
+ expect(Dir).to receive(:exist?).with('/configs' + '/projects')
212
+ .and_return(true)
213
+ expect(Dir).to receive(:children).with('/configs' + '/projects')
214
+ .and_return(projects)
215
+ expect(Dir).to receive(:children).with('/configs' + '/platforms')
216
+ .and_return(platforms)
217
+ expect do
218
+ cli.run(options_configdir)
219
+ end.to output(output_both).to_stdout
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+
226
+