kuber_kit 0.3.8 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +35 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/TODO.md +2 -1
- data/example/infrastructure/artifacts.rb +1 -1
- data/example/services/docker_app.rb +12 -0
- data/lib/kuber_kit.rb +6 -4
- data/lib/kuber_kit/actions/configuration_loader.rb +12 -19
- data/lib/kuber_kit/actions/env_file_reader.rb +1 -0
- data/lib/kuber_kit/actions/image_compiler.rb +10 -6
- data/lib/kuber_kit/actions/service_deployer.rb +10 -7
- data/lib/kuber_kit/actions/template_reader.rb +1 -0
- data/lib/kuber_kit/artifacts_sync/artifacts_updater.rb +3 -2
- data/lib/kuber_kit/cli.rb +38 -25
- data/lib/kuber_kit/configs.rb +2 -1
- data/lib/kuber_kit/container.rb +6 -2
- data/lib/kuber_kit/core/configuration_store.rb +2 -2
- data/lib/kuber_kit/core/image_store.rb +2 -2
- data/lib/kuber_kit/core/service.rb +5 -5
- data/lib/kuber_kit/core/service_factory.rb +0 -6
- data/lib/kuber_kit/core/service_store.rb +2 -2
- data/lib/kuber_kit/image_compiler/compiler.rb +3 -1
- data/lib/kuber_kit/image_compiler/image_builder.rb +9 -1
- data/lib/kuber_kit/service_deployer/strategies/docker.rb +26 -9
- data/lib/kuber_kit/service_deployer/strategies/docker_compose.rb +3 -0
- data/lib/kuber_kit/service_deployer/strategies/kubernetes.rb +3 -1
- data/lib/kuber_kit/service_reader/reader.rb +6 -0
- data/lib/kuber_kit/shell/commands/docker_commands.rb +59 -14
- data/lib/kuber_kit/shell/commands/docker_compose_commands.rb +4 -5
- data/lib/kuber_kit/shell/local_shell.rb +6 -6
- data/lib/kuber_kit/shell/ssh_shell.rb +4 -4
- data/lib/kuber_kit/tools/logger_factory.rb +7 -3
- data/lib/kuber_kit/ui/api.rb +48 -0
- data/lib/kuber_kit/ui/debug.rb +31 -0
- data/lib/kuber_kit/ui/interactive.rb +15 -0
- data/lib/kuber_kit/ui/simple.rb +34 -5
- data/lib/kuber_kit/version.rb +1 -1
- metadata +10 -6
data/lib/kuber_kit/configs.rb
CHANGED
@@ -5,7 +5,7 @@ class KuberKit::Configs
|
|
5
5
|
:image_dockerfile_name, :image_build_context_dir, :image_tag, :docker_ignore_list, :image_compile_dir,
|
6
6
|
:kuber_kit_dirname, :kuber_kit_min_version, :images_dirname, :services_dirname, :infra_dirname, :configurations_dirname,
|
7
7
|
:artifact_clone_dir, :service_config_dir, :deployer_strategy, :compile_simultaneous_limit,
|
8
|
-
:additional_images_paths, :deprecation_warnings_disabled
|
8
|
+
:additional_images_paths, :deprecation_warnings_disabled, :log_file_path
|
9
9
|
]
|
10
10
|
DOCKER_IGNORE_LIST = [
|
11
11
|
'Dockerfile',
|
@@ -51,6 +51,7 @@ class KuberKit::Configs
|
|
51
51
|
set :compile_simultaneous_limit, 5
|
52
52
|
set :additional_images_paths, []
|
53
53
|
set :deprecation_warnings_disabled, false
|
54
|
+
set :log_file_path, "/tmp/kuber_kit.log"
|
54
55
|
end
|
55
56
|
|
56
57
|
def items
|
data/lib/kuber_kit/container.rb
CHANGED
@@ -114,7 +114,7 @@ class KuberKit::Container
|
|
114
114
|
end
|
115
115
|
|
116
116
|
register "tools.logger" do
|
117
|
-
KuberKit::Container["tools.logger_factory"].create(
|
117
|
+
KuberKit::Container["tools.logger_factory"].create()
|
118
118
|
end
|
119
119
|
|
120
120
|
register "shell.bash_commands" do
|
@@ -258,8 +258,12 @@ class KuberKit::Container
|
|
258
258
|
end
|
259
259
|
|
260
260
|
register "ui" do
|
261
|
-
if KuberKit.
|
261
|
+
if KuberKit.ui_mode == :debug
|
262
|
+
KuberKit::UI::Debug.new
|
263
|
+
elsif KuberKit.ui_mode == :simple
|
262
264
|
KuberKit::UI::Simple.new
|
265
|
+
elsif KuberKit.ui_mode == :api
|
266
|
+
KuberKit::UI::Api.new
|
263
267
|
else
|
264
268
|
KuberKit::UI::Interactive.new
|
265
269
|
end
|
@@ -3,7 +3,7 @@ class KuberKit::Core::ConfigurationStore
|
|
3
3
|
"core.configuration_factory",
|
4
4
|
"core.configuration_definition_factory",
|
5
5
|
"shell.local_shell",
|
6
|
-
"
|
6
|
+
"ui"
|
7
7
|
]
|
8
8
|
|
9
9
|
def define(configuration_name)
|
@@ -33,7 +33,7 @@ class KuberKit::Core::ConfigurationStore
|
|
33
33
|
load_definition(path)
|
34
34
|
end
|
35
35
|
rescue KuberKit::Shell::AbstractShell::DirNotFoundError
|
36
|
-
|
36
|
+
ui.print_warning("ConfigurationStore", "Directory with configurations not found: #{dir_path}")
|
37
37
|
[]
|
38
38
|
end
|
39
39
|
|
@@ -3,7 +3,7 @@ class KuberKit::Core::ImageStore
|
|
3
3
|
"core.image_factory",
|
4
4
|
"core.image_definition_factory",
|
5
5
|
"shell.local_shell",
|
6
|
-
"
|
6
|
+
"ui"
|
7
7
|
]
|
8
8
|
|
9
9
|
def define(image_name, image_dir = nil)
|
@@ -33,7 +33,7 @@ class KuberKit::Core::ImageStore
|
|
33
33
|
load_definition(path)
|
34
34
|
end
|
35
35
|
rescue KuberKit::Shell::AbstractShell::DirNotFoundError
|
36
|
-
|
36
|
+
ui.print_warning("ImageStore", "Directory with images not found: #{dir_path}")
|
37
37
|
[]
|
38
38
|
end
|
39
39
|
|
@@ -4,11 +4,11 @@ class KuberKit::Core::Service
|
|
4
4
|
attr_reader :name, :template_name, :tags, :images, :attributes, :deployer_strategy
|
5
5
|
|
6
6
|
Contract KeywordArgs[
|
7
|
-
name:
|
8
|
-
template_name:
|
9
|
-
tags:
|
10
|
-
images:
|
11
|
-
attributes:
|
7
|
+
name: Symbol,
|
8
|
+
template_name: Maybe[Symbol],
|
9
|
+
tags: ArrayOf[Symbol],
|
10
|
+
images: ArrayOf[Symbol],
|
11
|
+
attributes: HashOf[Symbol => Any],
|
12
12
|
deployer_strategy: Maybe[Symbol]
|
13
13
|
] => Any
|
14
14
|
def initialize(name:, template_name:, tags:, images:, attributes:, deployer_strategy:)
|
@@ -1,13 +1,7 @@
|
|
1
1
|
class KuberKit::Core::ServiceFactory
|
2
|
-
AttributeNotSetError = Class.new(KuberKit::Error)
|
3
|
-
|
4
2
|
def create(definition)
|
5
3
|
service_attrs = definition.to_service_attrs
|
6
4
|
|
7
|
-
if service_attrs.template_name.nil?
|
8
|
-
raise AttributeNotSetError, "Please set template for service using #template method"
|
9
|
-
end
|
10
|
-
|
11
5
|
configuration_attributes = KuberKit.current_configuration.service_attributes(service_attrs.name)
|
12
6
|
attributes = (service_attrs.attributes || {}).merge(configuration_attributes)
|
13
7
|
|
@@ -3,7 +3,7 @@ class KuberKit::Core::ServiceStore
|
|
3
3
|
"core.service_factory",
|
4
4
|
"core.service_definition_factory",
|
5
5
|
"shell.local_shell",
|
6
|
-
"
|
6
|
+
"ui",
|
7
7
|
]
|
8
8
|
|
9
9
|
def define(service_name)
|
@@ -33,7 +33,7 @@ class KuberKit::Core::ServiceStore
|
|
33
33
|
load_definition(path)
|
34
34
|
end
|
35
35
|
rescue KuberKit::Shell::AbstractShell::DirNotFoundError
|
36
|
-
|
36
|
+
ui.print_warning("ServiceStore", "Directory with services not found: #{dir_path}")
|
37
37
|
[]
|
38
38
|
end
|
39
39
|
|
@@ -12,7 +12,9 @@ class KuberKit::ImageCompiler::Compiler
|
|
12
12
|
context_helper = context_helper_factory.build_image_context(shell, image)
|
13
13
|
image_build_dir_creator.create(shell, image, image_build_dir, context_helper: context_helper)
|
14
14
|
|
15
|
-
image_builder.build(shell, image, image_build_dir, context_helper: context_helper)
|
15
|
+
result = image_builder.build(shell, image, image_build_dir, context_helper: context_helper)
|
16
16
|
image_build_dir_creator.cleanup(shell, image_build_dir)
|
17
|
+
|
18
|
+
result
|
17
19
|
end
|
18
20
|
end
|
@@ -10,7 +10,13 @@ class KuberKit::ImageCompiler::ImageBuilder
|
|
10
10
|
def build(shell, image, build_dir, context_helper: nil)
|
11
11
|
image.before_build_callback.call(context_helper, build_dir) if image.before_build_callback
|
12
12
|
|
13
|
-
|
13
|
+
build_options = ["-t=#{image.registry_url}"]
|
14
|
+
# use quite option for api mode ui, so it will only return built image id
|
15
|
+
if KuberKit.ui_mode == :api
|
16
|
+
build_options << "-q"
|
17
|
+
end
|
18
|
+
|
19
|
+
build_result = docker_commands.build(shell, build_dir, build_options)
|
14
20
|
|
15
21
|
version_tag = version_tag_builder.get_version
|
16
22
|
docker_commands.tag(shell, image.registry_url, version_tag)
|
@@ -21,5 +27,7 @@ class KuberKit::ImageCompiler::ImageBuilder
|
|
21
27
|
end
|
22
28
|
|
23
29
|
image.after_build_callback.call(context_helper, build_dir) if image.after_build_callback
|
30
|
+
|
31
|
+
build_result
|
24
32
|
end
|
25
33
|
end
|
@@ -9,9 +9,11 @@ class KuberKit::ServiceDeployer::Strategies::Docker < KuberKit::ServiceDeployer:
|
|
9
9
|
:container_name,
|
10
10
|
:image_name,
|
11
11
|
:detached,
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:delete_if_exists
|
12
|
+
:command_name,
|
13
|
+
:custom_args,
|
14
|
+
:delete_if_exists,
|
15
|
+
:volumes,
|
16
|
+
:networks,
|
15
17
|
]
|
16
18
|
|
17
19
|
Contract KuberKit::Shell::AbstractShell, KuberKit::Core::Service => Any
|
@@ -22,9 +24,11 @@ class KuberKit::ServiceDeployer::Strategies::Docker < KuberKit::ServiceDeployer:
|
|
22
24
|
raise KuberKit::Error, "Unknow options for deploy strategy: #{unknown_options}. Available options: #{STRATEGY_OPTIONS}"
|
23
25
|
end
|
24
26
|
|
25
|
-
container_name
|
26
|
-
|
27
|
-
|
27
|
+
container_name = strategy_options.fetch(:container_name, service.uri)
|
28
|
+
command_name = strategy_options.fetch(:command_name, "bash")
|
29
|
+
custom_args = strategy_options.fetch(:custom_args, nil)
|
30
|
+
networks = strategy_options.fetch(:networks, [])
|
31
|
+
volumes = strategy_options.fetch(:volumes, [])
|
28
32
|
|
29
33
|
image_name = strategy_options.fetch(:image_name, nil)
|
30
34
|
if image_name.nil?
|
@@ -37,11 +41,24 @@ class KuberKit::ServiceDeployer::Strategies::Docker < KuberKit::ServiceDeployer:
|
|
37
41
|
docker_commands.delete_container(shell, container_name)
|
38
42
|
end
|
39
43
|
|
44
|
+
custom_args = Array(custom_args)
|
45
|
+
if container_name
|
46
|
+
custom_args << "--name #{container_name}"
|
47
|
+
end
|
48
|
+
networks.each do |network|
|
49
|
+
docker_commands.create_network(shell, network)
|
50
|
+
custom_args << "--network #{network}"
|
51
|
+
end
|
52
|
+
volumes.each do |volume|
|
53
|
+
docker_commands.create_volume(shell, volume)
|
54
|
+
custom_args << "--volume #{volume}"
|
55
|
+
end
|
56
|
+
|
40
57
|
docker_commands.run(
|
41
58
|
shell, image.remote_registry_url,
|
42
|
-
|
43
|
-
|
44
|
-
detached:
|
59
|
+
command: command_name,
|
60
|
+
args: custom_args,
|
61
|
+
detached: !!strategy_options[:detached]
|
45
62
|
)
|
46
63
|
end
|
47
64
|
end
|
@@ -8,6 +8,7 @@ class KuberKit::ServiceDeployer::Strategies::DockerCompose < KuberKit::ServiceDe
|
|
8
8
|
STRATEGY_OPTIONS = [
|
9
9
|
:service_name,
|
10
10
|
:command_name,
|
11
|
+
:custom_args,
|
11
12
|
:detached
|
12
13
|
]
|
13
14
|
|
@@ -25,10 +26,12 @@ class KuberKit::ServiceDeployer::Strategies::DockerCompose < KuberKit::ServiceDe
|
|
25
26
|
|
26
27
|
service_name = strategy_options.fetch(:service_name, service.name.to_s)
|
27
28
|
command_name = strategy_options.fetch(:command_name, "bash")
|
29
|
+
custom_args = strategy_options.fetch(:custom_args, nil)
|
28
30
|
|
29
31
|
docker_compose_commands.run(shell, config_path,
|
30
32
|
service: service_name,
|
31
33
|
command: command_name,
|
34
|
+
args: custom_args,
|
32
35
|
detached: !!strategy_options[:detached]
|
33
36
|
)
|
34
37
|
end
|
@@ -39,7 +39,7 @@ class KuberKit::ServiceDeployer::Strategies::Kubernetes < KuberKit::ServiceDeplo
|
|
39
39
|
kubectl_commands.delete_resource(shell, resource_type, resource_name, kubeconfig_path: kubeconfig_path, namespace: namespace)
|
40
40
|
end
|
41
41
|
|
42
|
-
kubectl_commands.apply_file(shell, config_path, kubeconfig_path: kubeconfig_path, namespace: namespace)
|
42
|
+
apply_result = kubectl_commands.apply_file(shell, config_path, kubeconfig_path: kubeconfig_path, namespace: namespace)
|
43
43
|
|
44
44
|
restart_enabled = strategy_options.fetch(:restart_if_exists, true)
|
45
45
|
if restart_enabled && resource_exists
|
@@ -48,5 +48,7 @@ class KuberKit::ServiceDeployer::Strategies::Kubernetes < KuberKit::ServiceDeplo
|
|
48
48
|
kubeconfig_path: kubeconfig_path, namespace: namespace
|
49
49
|
)
|
50
50
|
end
|
51
|
+
|
52
|
+
apply_result
|
51
53
|
end
|
52
54
|
end
|
@@ -6,8 +6,14 @@ class KuberKit::ServiceReader::Reader
|
|
6
6
|
"preprocessing.text_preprocessor"
|
7
7
|
]
|
8
8
|
|
9
|
+
AttributeNotSetError = Class.new(KuberKit::Error)
|
10
|
+
|
9
11
|
Contract KuberKit::Shell::AbstractShell, KuberKit::Core::Service => Any
|
10
12
|
def read(shell, service)
|
13
|
+
if service.template_name.nil?
|
14
|
+
raise AttributeNotSetError, "Please set template for service using #template method"
|
15
|
+
end
|
16
|
+
|
11
17
|
template = template_store.get(service.template_name)
|
12
18
|
|
13
19
|
context_helper = context_helper_factory.build_service_context(shell, service)
|
@@ -3,7 +3,7 @@ class KuberKit::Shell::Commands::DockerCommands
|
|
3
3
|
default_args = ["--rm=true"]
|
4
4
|
args_list = (default_args + args).join(" ")
|
5
5
|
|
6
|
-
shell.exec!(%Q{docker build #{build_dir} #{args_list}})
|
6
|
+
shell.exec!(%Q{docker image build #{build_dir} #{args_list}})
|
7
7
|
end
|
8
8
|
|
9
9
|
def tag(shell, image_name, tag_name)
|
@@ -14,37 +14,82 @@ class KuberKit::Shell::Commands::DockerCommands
|
|
14
14
|
shell.exec!(%Q{docker push #{tag_name}})
|
15
15
|
end
|
16
16
|
|
17
|
-
def run(shell, image_name,
|
17
|
+
def run(shell, image_name, args: nil, command: nil, detached: false, interactive: false)
|
18
18
|
command_parts = []
|
19
19
|
command_parts << "docker run"
|
20
20
|
command_parts << "-d" if detached
|
21
|
-
command_parts <<
|
21
|
+
command_parts << Array(args).join(" ") if args
|
22
22
|
command_parts << image_name
|
23
|
-
command_parts <<
|
23
|
+
command_parts << command if command
|
24
24
|
|
25
|
-
|
25
|
+
if interactive
|
26
|
+
shell.interactive!(command_parts.join(" "))
|
27
|
+
else
|
28
|
+
shell.exec!(command_parts.join(" "))
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
|
-
def container_exists?(shell, container_name)
|
29
|
-
result =
|
32
|
+
def container_exists?(shell, container_name, status: nil)
|
33
|
+
result = get_containers(shell, container_name, status: status)
|
30
34
|
result && result != ""
|
31
35
|
end
|
32
36
|
|
33
|
-
def
|
34
|
-
shell.exec!(%Q{docker rm -f #{container_name}})
|
35
|
-
end
|
36
|
-
|
37
|
-
def get_container_id(shell, container_name, only_healthy: false, status: "running")
|
37
|
+
def get_containers(shell, container_name, only_healthy: false, status: nil)
|
38
38
|
command_parts = []
|
39
39
|
command_parts << "docker ps -a -q"
|
40
40
|
|
41
41
|
if only_healthy
|
42
42
|
command_parts << "--filter=\"health=healthy\""
|
43
43
|
end
|
44
|
-
|
45
|
-
|
44
|
+
if status
|
45
|
+
command_parts << "--filter=\"status=#{status}\""
|
46
|
+
end
|
46
47
|
command_parts << "--filter=\"name=#{container_name}\""
|
47
48
|
|
48
49
|
shell.exec!(command_parts.join(" "))
|
49
50
|
end
|
51
|
+
|
52
|
+
def delete_container(shell, container_name)
|
53
|
+
shell.exec!("docker rm -f #{container_name}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_network(shell, name)
|
57
|
+
unless network_exists?(shell, name)
|
58
|
+
shell.exec!("docker network create #{name}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def network_exists?(shell, network_name)
|
63
|
+
result = get_networks(shell, network_name)
|
64
|
+
result && result != ""
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_networks(shell, network_name)
|
68
|
+
command_parts = []
|
69
|
+
command_parts << "docker network ls"
|
70
|
+
command_parts << "--filter=\"name=#{network_name}\""
|
71
|
+
command_parts << "--format \"{{.Name}}\""
|
72
|
+
|
73
|
+
shell.exec!(command_parts.join(" "))
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_volume(shell, name)
|
77
|
+
unless volume_exists?(shell, name)
|
78
|
+
shell.exec!("docker volume create #{name}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def volume_exists?(shell, volume_name)
|
83
|
+
result = get_volumes(shell, volume_name)
|
84
|
+
result && result != ""
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_volumes(shell, volume_name)
|
88
|
+
command_parts = []
|
89
|
+
command_parts << "docker volume ls"
|
90
|
+
command_parts << "--filter=\"name=#{volume_name}\""
|
91
|
+
command_parts << "--format \"{{.Name}}\""
|
92
|
+
|
93
|
+
shell.exec!(command_parts.join(" "))
|
94
|
+
end
|
50
95
|
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
class KuberKit::Shell::Commands::DockerComposeCommands
|
2
|
-
def run(shell, path, service:, command
|
2
|
+
def run(shell, path, service:, args: nil, command: nil, detached: false, interactive: false)
|
3
3
|
command_parts = [
|
4
4
|
"docker-compose",
|
5
5
|
"-f #{path}",
|
6
6
|
"run",
|
7
7
|
]
|
8
8
|
|
9
|
-
if detached
|
10
|
-
command_parts << "-d"
|
11
|
-
end
|
12
9
|
|
10
|
+
command_parts << "-d" if detached
|
11
|
+
command_parts << Array(args).join(" ") if args
|
13
12
|
command_parts << service
|
14
|
-
command_parts << command
|
13
|
+
command_parts << command if command
|
15
14
|
|
16
15
|
if interactive
|
17
16
|
shell.interactive!(command_parts.join(" "))
|
@@ -2,16 +2,16 @@ require 'fileutils'
|
|
2
2
|
|
3
3
|
class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
|
4
4
|
include KuberKit::Import[
|
5
|
-
"tools.logger",
|
6
5
|
"shell.command_counter",
|
7
6
|
"shell.rsync_commands",
|
7
|
+
"ui",
|
8
8
|
]
|
9
9
|
|
10
10
|
def exec!(command, log_command: true)
|
11
11
|
command_number = command_counter.get_number.to_s.rjust(2, "0")
|
12
12
|
|
13
13
|
if log_command
|
14
|
-
|
14
|
+
ui.print_debug("LocalShell", "Execute: [#{command_number}]: #{command.to_s.cyan}")
|
15
15
|
end
|
16
16
|
|
17
17
|
result = nil
|
@@ -20,7 +20,7 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
|
|
20
20
|
end
|
21
21
|
|
22
22
|
if result && result != "" && log_command
|
23
|
-
|
23
|
+
ui.print_debug("LocalShell", "Finished [#{command_number}] with result: \n ----\n#{result.grey}\n ----")
|
24
24
|
end
|
25
25
|
|
26
26
|
if $?.exitstatus != 0
|
@@ -34,7 +34,7 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
|
|
34
34
|
command_number = command_counter.get_number.to_s.rjust(2, "0")
|
35
35
|
|
36
36
|
if log_command
|
37
|
-
|
37
|
+
ui.print_debug("LocalShell", "Interactive: [#{command_number}]: #{command.to_s.cyan}")
|
38
38
|
end
|
39
39
|
|
40
40
|
result = system(command)
|
@@ -57,7 +57,7 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
|
|
57
57
|
|
58
58
|
File.write(file_path, content)
|
59
59
|
|
60
|
-
|
60
|
+
ui.print_debug("LocalShell", "Created file #{file_path.to_s.cyan}\r\n ----\r\n#{content.grey}\r\n ----")
|
61
61
|
|
62
62
|
true
|
63
63
|
end
|
@@ -76,7 +76,7 @@ class KuberKit::Shell::LocalShell < KuberKit::Shell::AbstractShell
|
|
76
76
|
|
77
77
|
def recursive_list_files(path, name: nil)
|
78
78
|
command = %Q{find -L #{path} -type f}
|
79
|
-
command += " -name #{name}" if name
|
79
|
+
command += " -name '#{name}'" if name
|
80
80
|
exec!(command).split(/[\r\n]+/)
|
81
81
|
rescue => e
|
82
82
|
if e.message.include?("No such file or directory")
|