bosh-workspace 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bosh-workspace.gemspec +5 -4
- data/lib/bosh/cli/commands/deployment_patch.rb +0 -1
- data/lib/bosh/cli/commands/prepare.rb +12 -11
- data/lib/bosh/workspace.rb +4 -3
- data/lib/bosh/workspace/credentials.rb +12 -5
- data/lib/bosh/workspace/git_credentials_provider.rb +80 -0
- data/lib/bosh/workspace/{git_remote_url.rb → helpers/git_protocol_helper.rb} +4 -8
- data/lib/bosh/workspace/helpers/project_deployment_helper.rb +10 -2
- data/lib/bosh/workspace/helpers/release_helper.rb +33 -7
- data/lib/bosh/workspace/manifest_builder.rb +6 -8
- data/lib/bosh/workspace/merge_tool.rb +73 -0
- data/lib/bosh/workspace/project_deployment.rb +20 -5
- data/lib/bosh/workspace/release.rb +103 -67
- data/lib/bosh/workspace/schemas/credentials.rb +22 -0
- data/lib/bosh/workspace/schemas/project_deployment.rb +14 -1
- data/lib/bosh/workspace/version.rb +1 -1
- data/spec/assets/bin/spruce +0 -0
- data/spec/assets/manifests-repo/deployments/foo.yml +1 -0
- data/spec/assets/manifests-repo/stubs/foo.yml +4 -0
- data/spec/commands/prepare_spec.rb +39 -12
- data/spec/credentials_spec.rb +8 -0
- data/spec/git_credentials_provider_spec.rb +82 -0
- data/spec/{git_remote_url_spec.rb → helpers/git_protocol_helper_spec.rb} +10 -11
- data/spec/helpers/project_deployment_helper_spec.rb +12 -1
- data/spec/helpers/release_helper_spec.rb +157 -73
- data/spec/manifest_builder_spec.rb +6 -5
- data/spec/merge_tool_spec.rb +98 -0
- data/spec/project_deployment_spec.rb +43 -1
- data/spec/release_spec.rb +369 -354
- data/spec/rugged_spec.rb +64 -0
- data/spec/schemas/credentials_spec.rb +22 -5
- data/spec/spec_helper.rb +1 -0
- metadata +35 -15
- data/lib/bosh/workspace/helpers/git_credentials_helper.rb +0 -111
- data/lib/bosh/workspace/helpers/spiff_helper.rb +0 -34
- data/spec/helpers/git_credentials_helper_spec.rb +0 -190
- data/spec/helpers/spiff_helper_spec.rb +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 095b4cec01561a415d908bc83c6a86e5a6861bac
|
4
|
+
data.tar.gz: 3038bc5ecd5756cf3672bbdc15405eee574a39c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 835a7af3d529d8264b273157ce4c2f80e5cdbe73ed3c8b817f469edebd874e730ff64110942b2f2aeb962930ac4fb0a3c39ef62445d9e0bb4c5ebbc04f32876c
|
7
|
+
data.tar.gz: 75e12938c90cea7260a8c2d16a6098d6c189bf9569431bf889e5cce7b23c70b57fafe297aa396b205e70cc54dc40adba7f47ec755007a9408cb60e3cd78bbd7d
|
data/bosh-workspace.gemspec
CHANGED
@@ -20,16 +20,17 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.required_ruby_version = '>= 2.0.0'
|
22
22
|
|
23
|
-
spec.add_runtime_dependency "bosh_cli",
|
24
|
-
spec.add_runtime_dependency "bosh_common",
|
23
|
+
spec.add_runtime_dependency "bosh_cli", ">= 1.2905.0"
|
24
|
+
spec.add_runtime_dependency "bosh_common", ">= 1.2905.0"
|
25
|
+
spec.add_runtime_dependency "bosh-template", ">= 1.2905.0"
|
25
26
|
spec.add_runtime_dependency "semi_semantic", "~> 1.1.0"
|
26
27
|
spec.add_runtime_dependency "membrane", "~> 1.1.0"
|
27
28
|
spec.add_runtime_dependency "hashdiff", "~> 0.2.1"
|
28
29
|
spec.add_runtime_dependency "rugged", "~> 0.23.0b3"
|
29
30
|
|
30
31
|
spec.add_development_dependency "bundler", "~> 1.6"
|
31
|
-
spec.add_development_dependency "rspec", "~> 3.
|
32
|
-
spec.add_development_dependency "rspec-its", '~> 1.
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.3.0"
|
33
|
+
spec.add_development_dependency "rspec-its", '~> 1.2.0'
|
33
34
|
spec.add_development_dependency "rake"
|
34
35
|
spec.add_development_dependency "archive-zip", "~> 0.7.0"
|
35
36
|
end
|
@@ -3,7 +3,6 @@ require "bosh/workspace"
|
|
3
3
|
module Bosh::Cli::Command
|
4
4
|
class DeploymentPatch < Base
|
5
5
|
include Bosh::Workspace::ProjectDeploymentHelper
|
6
|
-
include Bosh::Workspace::GitCredentialsHelper
|
7
6
|
|
8
7
|
usage "create deployment patch"
|
9
8
|
desc "Extract patch from the current directory and optionally writes to file"
|
@@ -5,15 +5,16 @@ module Bosh::Cli::Command
|
|
5
5
|
include Bosh::Cli::Validation
|
6
6
|
include Bosh::Workspace
|
7
7
|
include ProjectDeploymentHelper
|
8
|
-
include GitCredentialsHelper
|
9
8
|
include ReleaseHelper
|
10
9
|
include StemcellHelper
|
11
10
|
|
12
11
|
usage "prepare deployment"
|
13
12
|
desc "Resolve deployment requirements"
|
13
|
+
option "--local", "only perform local git operations (don't fetch remote)"
|
14
14
|
def prepare
|
15
15
|
require_project_deployment
|
16
16
|
auth_required
|
17
|
+
offline! if options[:local]
|
17
18
|
nl
|
18
19
|
prepare_release_repos
|
19
20
|
nl
|
@@ -28,19 +29,18 @@ module Bosh::Cli::Command
|
|
28
29
|
project_deployment_releases.each do |release|
|
29
30
|
require_git_url_error if release.git_url.nil?
|
30
31
|
say "Fetching release '#{release.name.make_green}' to satisfy template references"
|
31
|
-
fetch_or_clone_repo(release.repo_dir, release.git_url)
|
32
32
|
release.update_repo
|
33
|
-
release
|
34
|
-
fetch_or_clone_repo(File.join(release.repo_dir, submodule.path), submodule.url)
|
35
|
-
release.update_submodule(submodule)
|
36
|
-
end
|
37
|
-
msg = "Version '#{release.version.to_s.make_green}'"
|
38
|
-
msg = "Ref '#{release.ref.make_green}'" if release.ref
|
39
|
-
say "#{msg} has been checkout into:"
|
40
|
-
say "- #{release.repo_dir}"
|
33
|
+
print_prepare_release_repo_message(release)
|
41
34
|
end
|
42
35
|
end
|
43
36
|
|
37
|
+
def print_prepare_release_repo_message(release)
|
38
|
+
msg = "Version '#{release.version.to_s.make_green}'"
|
39
|
+
msg = "Ref '#{release.ref.make_green}'" if release.ref
|
40
|
+
say "#{msg} has been checkout into:"
|
41
|
+
say "- #{release.repo_dir}"
|
42
|
+
end
|
43
|
+
|
44
44
|
def require_git_url_error
|
45
45
|
say "`bosh prepare deployment' can not be used:"
|
46
46
|
err("`git:' is missing from `release:'".make_red)
|
@@ -58,7 +58,7 @@ module Bosh::Cli::Command
|
|
58
58
|
say "Skipping upload"
|
59
59
|
elsif release.url
|
60
60
|
say "Uploading '#{release.url}'"
|
61
|
-
|
61
|
+
release_upload_from_url(release.url)
|
62
62
|
else
|
63
63
|
say "Uploading '#{release.name_version.make_green}'"
|
64
64
|
release_upload(release.manifest_file, release.release_dir)
|
@@ -82,6 +82,7 @@ module Bosh::Cli::Command
|
|
82
82
|
|
83
83
|
def cached_stemcell_upload(stemcell)
|
84
84
|
unless stemcell.downloaded?
|
85
|
+
err "Stemcell not available offline: #{stemcell.file_name}" if offline?
|
85
86
|
say "Downloading '#{stemcell.name_version.make_green}'"
|
86
87
|
stemcell_download(stemcell.file_name)
|
87
88
|
end
|
data/lib/bosh/workspace.rb
CHANGED
@@ -6,14 +6,15 @@ require "rugged"
|
|
6
6
|
require "hashdiff"
|
7
7
|
require "cli/core_ext"
|
8
8
|
require "cli/validation"
|
9
|
+
require "bosh/template/renderer"
|
9
10
|
|
10
|
-
require "bosh/workspace/helpers/spiff_helper"
|
11
11
|
require "bosh/workspace/helpers/project_deployment_helper"
|
12
|
-
require "bosh/workspace/helpers/git_credentials_helper"
|
13
12
|
require "bosh/workspace/helpers/release_helper"
|
14
13
|
require "bosh/workspace/helpers/stemcell_helper"
|
15
14
|
require "bosh/workspace/helpers/dns_helper"
|
15
|
+
require "bosh/workspace/helpers/git_protocol_helper"
|
16
16
|
|
17
|
+
require "bosh/workspace/merge_tool"
|
17
18
|
require "bosh/workspace/schemas/project_deployment"
|
18
19
|
require "bosh/workspace/schemas/deployment_patch"
|
19
20
|
require "bosh/workspace/schemas/releases"
|
@@ -28,5 +29,5 @@ require "bosh/workspace/project_deployment"
|
|
28
29
|
require "bosh/workspace/stub_file"
|
29
30
|
require "bosh/workspace/deployment_patch"
|
30
31
|
require "bosh/workspace/credentials"
|
31
|
-
require "bosh/workspace/
|
32
|
+
require "bosh/workspace/git_credentials_provider"
|
32
33
|
require "bosh/workspace/version"
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module Bosh::Workspace
|
2
2
|
class Credentials
|
3
3
|
include Bosh::Cli::Validation
|
4
|
+
include GitProtocolHelper
|
5
|
+
attr_reader :raw_credentials
|
4
6
|
|
5
7
|
def initialize(file)
|
6
8
|
@raw_credentials = YAML.load_file file
|
7
9
|
end
|
8
10
|
|
9
11
|
def perform_validation(options = {})
|
10
|
-
Schemas::Credentials.new.validate
|
12
|
+
Schemas::Credentials.new.validate raw_credentials
|
11
13
|
rescue Membrane::SchemaValidationError => e
|
12
14
|
errors << e.message
|
13
15
|
end
|
@@ -16,15 +18,20 @@ module Bosh::Workspace
|
|
16
18
|
credentials[url]
|
17
19
|
end
|
18
20
|
|
21
|
+
def url_protocols
|
22
|
+
Hash[raw_credentials.map { |c| [c['url'], git_protocol_from_url(c['url'])] }]
|
23
|
+
end
|
24
|
+
|
19
25
|
private
|
20
26
|
|
21
27
|
def credentials
|
22
28
|
@credentials ||= begin
|
23
|
-
Hash[
|
24
|
-
[c.delete('url'),
|
25
|
-
c.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }]
|
26
|
-
end]
|
29
|
+
Hash[raw_credentials.map { |c| [c.delete('url'), symbolize_keys(c)] }]
|
27
30
|
end
|
28
31
|
end
|
32
|
+
|
33
|
+
def symbolize_keys(hash)
|
34
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
35
|
+
end
|
29
36
|
end
|
30
37
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Bosh::Workspace
|
2
|
+
class GitCredentialsProvider
|
3
|
+
attr_reader :credentials_file
|
4
|
+
|
5
|
+
def initialize(credentials_file)
|
6
|
+
@credentials_file = credentials_file
|
7
|
+
end
|
8
|
+
|
9
|
+
def callback
|
10
|
+
proc do |url, user, allowed_types|
|
11
|
+
require_credentials_file_for!(url)
|
12
|
+
validate_credentials!
|
13
|
+
validate_url_protocol_support!
|
14
|
+
credentials_for(url, user, allowed_types)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def credentials
|
21
|
+
@credentials ||= Credentials.new(@credentials_file)
|
22
|
+
end
|
23
|
+
|
24
|
+
def require_credentials_file_for!(url)
|
25
|
+
return if File.exist? @credentials_file
|
26
|
+
say("Authentication is required for: #{url}".make_red)
|
27
|
+
err("Credentials file does not exist: #{@credentials_file}".make_red)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_credentials!
|
31
|
+
return if credentials.valid?
|
32
|
+
say("Validation errors:".make_red)
|
33
|
+
credentials.errors.each { |error| say("- #{error}") }
|
34
|
+
err("'#{credentials_file}' is not valid".make_red)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_url_protocol_support!
|
38
|
+
credentials.url_protocols.each do |url, protocol|
|
39
|
+
next if Rugged.features.include? protocol
|
40
|
+
say("Please reinstall Rugged gem with #{protocol} support: http://git.io/veiyJ")
|
41
|
+
err("Rugged requires #{protocol} support for: #{url}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def credentials_for(url, user, allowed_types)
|
46
|
+
if creds = credentials.find_by_url(url)
|
47
|
+
load_git_credentials(creds, user, allowed_types)
|
48
|
+
else
|
49
|
+
say("Credential look up failed in: #{credentials_file}")
|
50
|
+
err("No credentials found for: #{url}".make_red)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_git_credentials(credentials, user, allowed_types)
|
55
|
+
case allowed_types
|
56
|
+
when %i(ssh_key)
|
57
|
+
key_file = temp_key_file(credentials[:private_key])
|
58
|
+
Rugged::Credentials::SshKey.new username: user, privatekey: key_file
|
59
|
+
when %i(plain_text)
|
60
|
+
Rugged::Credentials::UserPassword.new(credentials)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def temp_key_file(key)
|
65
|
+
file = Tempfile.new('sshkey')
|
66
|
+
file.write key
|
67
|
+
file.close
|
68
|
+
File.chmod(0600, file.path)
|
69
|
+
file.path
|
70
|
+
end
|
71
|
+
|
72
|
+
def say(*args)
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
def err(*args)
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
module Bosh::Workspace
|
2
|
-
|
3
|
-
def
|
4
|
-
|
5
|
-
end
|
6
|
-
|
7
|
-
def protocol()
|
8
|
-
case @url
|
2
|
+
module GitProtocolHelper
|
3
|
+
def git_protocol_from_url(url)
|
4
|
+
case url
|
9
5
|
when /^git:/
|
10
6
|
return :git
|
11
7
|
when /^https:/
|
@@ -15,7 +11,7 @@ module Bosh::Workspace
|
|
15
11
|
when /(@.+:|^ssh:)/
|
16
12
|
return :ssh
|
17
13
|
else
|
18
|
-
|
14
|
+
return nil
|
19
15
|
end
|
20
16
|
end
|
21
17
|
end
|
@@ -15,7 +15,7 @@ module Bosh::Workspace
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def project_deployment_file?(deployment)
|
18
|
-
|
18
|
+
ProjectDeployment.new(deployment).manifest.has_key?("templates")
|
19
19
|
end
|
20
20
|
|
21
21
|
def require_project_deployment
|
@@ -46,7 +46,7 @@ module Bosh::Workspace
|
|
46
46
|
|
47
47
|
say("Generating deployment manifest")
|
48
48
|
ManifestBuilder.build(project_deployment, work_dir)
|
49
|
-
|
49
|
+
|
50
50
|
if domain_name = project_deployment.domain_name
|
51
51
|
say("Transforming to dynamic networking (dns)")
|
52
52
|
DnsHelper.transform(project_deployment.merged_file, domain_name)
|
@@ -57,6 +57,14 @@ module Bosh::Workspace
|
|
57
57
|
use_targeted_director_uuid if director_uuid_current?
|
58
58
|
end
|
59
59
|
|
60
|
+
def offline!
|
61
|
+
@offline = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def offline?
|
65
|
+
@offline
|
66
|
+
end
|
67
|
+
|
60
68
|
private
|
61
69
|
|
62
70
|
def use_targeted_director_uuid
|
@@ -7,10 +7,13 @@ module Bosh::Workspace
|
|
7
7
|
remote_release && remote_release["versions"].include?(version.to_s)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def release_upload_from_url(release_url)
|
11
|
+
upload_release_cmd.upload(release_url)
|
12
|
+
end
|
13
|
+
|
14
|
+
def release_upload(manifest_file, release_dir)
|
15
|
+
release_tarball = create_release(manifest_file, release_dir)
|
16
|
+
upload_release_cmd.upload(release_tarball)
|
14
17
|
end
|
15
18
|
|
16
19
|
def releases_dir
|
@@ -20,15 +23,38 @@ module Bosh::Workspace
|
|
20
23
|
end
|
21
24
|
|
22
25
|
def project_deployment_releases
|
23
|
-
|
24
|
-
|
26
|
+
project_deployment.releases.map do |r|
|
27
|
+
Release.new(r, releases_dir, credentials_callback, offline: offline?)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
28
31
|
private
|
29
32
|
|
30
|
-
def
|
33
|
+
def create_release(release_manifest, release_dir)
|
34
|
+
release_tarball = release_manifest.sub('yml', 'tgz')
|
35
|
+
return release_tarball if File.exist?(release_tarball)
|
36
|
+
err "Final release tarball missing: #{release_tarball}" if offline?
|
37
|
+
create_release_cmd(release_dir).create(release_manifest)
|
38
|
+
release_tarball
|
39
|
+
end
|
40
|
+
|
41
|
+
def credentials_callback
|
42
|
+
@callback ||= GitCredentialsProvider.new(credentials_file).callback
|
43
|
+
end
|
44
|
+
|
45
|
+
def credentials_file
|
46
|
+
File.join(work_dir, '.credentials.yml')
|
47
|
+
end
|
48
|
+
|
49
|
+
def upload_release_cmd
|
31
50
|
Bosh::Cli::Command::Release::UploadRelease.new
|
32
51
|
end
|
52
|
+
|
53
|
+
def create_release_cmd(release_dir)
|
54
|
+
Bosh::Cli::Command::Release::CreateRelease.new.tap do |r|
|
55
|
+
r.add_option(:with_tarball, true)
|
56
|
+
r.add_option(:dir, release_dir)
|
57
|
+
end
|
58
|
+
end
|
33
59
|
end
|
34
60
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Bosh::Workspace
|
2
2
|
class ManifestBuilder
|
3
|
-
|
3
|
+
attr_reader :merge_tool
|
4
4
|
|
5
5
|
def self.build(project_deployment, work_dir)
|
6
6
|
manifest_builder = ManifestBuilder.new(project_deployment, work_dir)
|
@@ -9,22 +9,20 @@ module Bosh::Workspace
|
|
9
9
|
|
10
10
|
def initialize(project_deployment, work_dir)
|
11
11
|
@project_deployment = project_deployment
|
12
|
+
@merge_tool = @project_deployment.merge_tool
|
12
13
|
@work_dir = work_dir
|
13
14
|
end
|
14
15
|
|
15
16
|
def merge_templates
|
16
|
-
|
17
|
+
@merge_tool.merge(template_paths, @project_deployment.merged_file)
|
17
18
|
end
|
18
19
|
|
19
20
|
private
|
20
21
|
|
21
|
-
def spiff_template_paths
|
22
|
-
spiff_templates = template_paths
|
23
|
-
spiff_templates << stub_file_path
|
24
|
-
end
|
25
|
-
|
26
22
|
def template_paths
|
27
|
-
@project_deployment.templates.map
|
23
|
+
@template_paths ||= @project_deployment.templates.map do |t|
|
24
|
+
template_path(t)
|
25
|
+
end.push(stub_file_path)
|
28
26
|
end
|
29
27
|
|
30
28
|
def stub_file_path
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Bosh::Workspace
|
2
|
+
class MergeTool
|
3
|
+
include Bosh::Exec
|
4
|
+
|
5
|
+
attr_accessor :name, :version
|
6
|
+
|
7
|
+
def initialize(merge_tool = nil)
|
8
|
+
@name, @version = case merge_tool
|
9
|
+
when Hash
|
10
|
+
[merge_tool['name'], merge_tool['version']]
|
11
|
+
when String
|
12
|
+
[merge_tool, 'current']
|
13
|
+
else
|
14
|
+
['spiff', 'current']
|
15
|
+
end
|
16
|
+
|
17
|
+
unless available_tool_names.include?(@name)
|
18
|
+
say("#{@name} is not supported, please specify spiff or spruce instead.".make_red)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def available_tool_names
|
23
|
+
%w(spiff spruce)
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(templates, target_file)
|
27
|
+
check_tool_version if version != 'current'
|
28
|
+
run_merge_tool(:merge, templates) do |output|
|
29
|
+
File.open(target_file, 'w') { |file| file.write(output) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def run_merge_tool(verb, params = [])
|
36
|
+
params.map!(&:shellescape)
|
37
|
+
cmd = [name, verb.to_s] + params + ['2>&1']
|
38
|
+
sh(cmd.join(" "), :yield => :on_false) do |result|
|
39
|
+
command_not_found if result.not_found?
|
40
|
+
command_failed(result.command, result.output) if result.failed?
|
41
|
+
yield result.output
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_tool_version
|
46
|
+
run_merge_tool('-v') do |output|
|
47
|
+
actual_version = output.match(/(\d+\.\d+\.\d+)/).to_a.first
|
48
|
+
if actual_version.nil? || actual_version != version
|
49
|
+
warning "Deployment requires #{name} to have version #{version}. " +
|
50
|
+
"Your actual #{name} version is #{actual_version}."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def command_not_found(command)
|
56
|
+
say("Can't find #{name} in $PATH".make_red)
|
57
|
+
say("Go to #{installation_instructions_url} for installation instructions")
|
58
|
+
err("Please make sure #{name} is installed")
|
59
|
+
end
|
60
|
+
|
61
|
+
def installation_instructions_url
|
62
|
+
case name
|
63
|
+
when 'spiff' then 'spiff.cfapps.io'
|
64
|
+
when 'spruce' then 'https://github.com/geofffranks/spruce#installation'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def command_failed(command, output)
|
69
|
+
say("Command failed: '#{command}'")
|
70
|
+
err(output)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|