From 71baf4eb59b277e4ec24232dff6415e43f24a0f8 Mon Sep 17 00:00:00 2001 From: Misha Merkushin Date: Tue, 30 Mar 2021 18:28:41 +0300 Subject: [PATCH] feat: implement command execution via through shell Close #113 https://www.rubydoc.info/stdlib/core/Process.exec --- lib/dip/command.rb | 15 ++++---- lib/dip/commands/run.rb | 12 ++++--- lib/dip/interaction_tree.rb | 17 ++------- spec/lib/dip/commands/dns_spec.rb | 49 +++++++++++++++----------- spec/lib/dip/commands/nginx_spec.rb | 20 +++++------ spec/lib/dip/commands/ssh_spec.rb | 18 ++++------ spec/support/shared_contexts/runner.rb | 6 ++-- 7 files changed, 66 insertions(+), 71 deletions(-) diff --git a/lib/dip/command.rb b/lib/dip/command.rb index a4ae09a..3265927 100644 --- a/lib/dip/command.rb +++ b/lib/dip/command.rb @@ -9,15 +9,15 @@ class Command def_delegators self, :shell, :subshell class ExecRunner - def self.call(cmd, argv, env: {}, **options) - ::Process.exec(env, cmd, *argv, options) + def self.call(cmdline, env: {}, **options) + ::Process.exec(env, cmdline, options) end end class SubshellRunner - def self.call(cmd, argv, env: {}, panic: true, **options) - return if ::Kernel.system(env, cmd, *argv, options) - raise Dip::Error, "Command '#{([cmd] + argv).join(' ')}' executed with error." if panic + def self.call(cmdline, env: {}, panic: true, **options) + return if ::Kernel.system(env, cmdline, options) + raise Dip::Error, "Command '#{cmdline}' executed with error." if panic end end @@ -25,11 +25,12 @@ class << self def shell(cmd, argv = [], subshell: false, **options) cmd = Dip.env.interpolate(cmd) argv = argv.map { |arg| Dip.env.interpolate(arg) } + cmdline = [cmd, *argv].compact.join(" ") - puts [Dip.env.vars, cmd, argv].inspect if Dip.debug? + puts [Dip.env.vars, cmdline].inspect if Dip.debug? runner = subshell ? SubshellRunner : ExecRunner - runner.call(cmd, argv, env: Dip.env.vars, **options) + runner.call(cmdline, env: Dip.env.vars, **options) end def subshell(*args, **kwargs) diff --git a/lib/dip/commands/run.rb b/lib/dip/commands/run.rb index fb86561..e67e626 100644 --- a/lib/dip/commands/run.rb +++ b/lib/dip/commands/run.rb @@ -44,11 +44,15 @@ def compose_arguments compose_argv << command.fetch(:service) - unless (cmd = command[:command].to_s).empty? - compose_argv.concat(cmd.shellsplit) + unless (cmd = command[:command]).empty? + compose_argv << cmd end - compose_argv.concat(argv.any? ? argv : command[:default_args]) + if argv.any? + compose_argv.concat(argv) + elsif !(default_args = command[:default_args]).empty? + compose_argv << default_args + end compose_argv end @@ -57,7 +61,7 @@ def run_vars run_vars = Dip::RunVars.env return [] unless run_vars - run_vars.map { |k, v| ["-e", "#{k}=#{v}"] }.flatten + run_vars.map { |k, v| ["-e", "#{k}=#{Shellwords.escape(v)}"] }.flatten end def published_ports diff --git a/lib/dip/interaction_tree.rb b/lib/dip/interaction_tree.rb index 6d0f527..a8febc1 100644 --- a/lib/dip/interaction_tree.rb +++ b/lib/dip/interaction_tree.rb @@ -59,8 +59,8 @@ def build_command(entry) { description: entry[:description], service: entry.fetch(:service), - command: entry[:command], - default_args: prepare_default_args(entry[:default_args]), + command: entry[:command].to_s.strip, + default_args: entry[:default_args].to_s.strip, environment: entry[:environment] || {}, compose: { method: entry.dig(:compose, :method) || entry[:compose_method] || "run", @@ -76,19 +76,6 @@ def sub_command_defaults!(entry) entry[:description] ||= nil end - def prepare_default_args(args) - return [] if args.nil? - - case args - when Array - args - when String - args.shellsplit - else - raise ArgumentError, "Unknown type for default_args: #{args.inspect}" - end - end - def compose_run_options(value) return [] unless value diff --git a/spec/lib/dip/commands/dns_spec.rb b/spec/lib/dip/commands/dns_spec.rb index 804f365..0e147cb 100644 --- a/spec/lib/dip/commands/dns_spec.rb +++ b/spec/lib/dip/commands/dns_spec.rb @@ -8,62 +8,69 @@ let(:cli) { Dip::CLI::DNS } describe Dip::Commands::DNS::Up do + let(:volume) { "--volume /var/run/docker.sock:/var/run/docker.sock:ro" } + let(:net) { "--net frontend" } + let(:name) { "--name dnsdock" } + let(:domain) { "--domain=docker" } + let(:port) { "--publish 53/udp" } + let(:image) { "aacebedo/dnsdock:latest-amd64" } + let(:cmd) { "run --detach #{volume} --restart always #{port} #{net} #{name} #{image} #{domain}" } + context "when without arguments" do before { cli.start "up".shellsplit } it { expected_subshell("docker", ["network", "create", "frontend"]) } - it do - expected_subshell( - "docker", - ["run", "--detach", "--volume", "/var/run/docker.sock:/var/run/docker.sock:ro", "--restart", "always", - "--publish", "53/udp", "--net", "frontend", "--name", "dnsdock", "aacebedo/dnsdock:latest-amd64", - "--domain=docker"] - ) - end + it { expected_subshell("docker", cmd) } end context "when option `name` is present" do + let(:name) { "--name foo" } before { cli.start "up --name foo".shellsplit } - it { expected_subshell("docker", array_including("--name", "foo")) } + it { expected_subshell("docker", cmd) } end context "when option `socket` is present" do + let(:volume) { "--volume foo:/var/run/docker.sock:ro" } before { cli.start "up --socket foo".shellsplit } - it { expected_subshell("docker", array_including("--volume", "foo:/var/run/docker.sock:ro")) } + it { expected_subshell("docker", cmd) } end context "when option `net` is present" do + let(:net) { "--net foo" } before { cli.start "up --net foo".shellsplit } it { expected_subshell("docker", ["network", "create", "foo"]) } - it { expected_subshell("docker", array_including("--net", "foo")) } + it { expected_subshell("docker", cmd) } end context "when option `publish` is present" do + let(:port) { "--publish foo" } before { cli.start "up --publish foo".shellsplit } - it { expected_subshell("docker", array_including("--publish", "foo")) } + it { expected_subshell("docker", cmd) } end context "when option `image` is present" do + let(:image) { "foo" } before { cli.start "up --image foo".shellsplit } - it { expected_subshell("docker", array_including("foo")) } + it { expected_subshell("docker", cmd) } end context "when option `domain` is present" do + let(:domain) { "--domain=foo" } before { cli.start "up --domain foo".shellsplit } - it { expected_subshell("docker", array_including("--domain=foo")) } + it { expected_subshell("docker", cmd) } end end describe Dip::Commands::DNS::Down do context "when without arguments" do before { cli.start "down".shellsplit } - it { expected_subshell("docker", ["stop", "dnsdock"]) } - it { expected_subshell("docker", ["rm", "-v", "dnsdock"]) } + it { expected_subshell("docker", "stop dnsdock") } + it { expected_subshell("docker", "rm -v dnsdock") } end context "when option `name` is present" do before { cli.start "down --name foo".shellsplit } - it { expected_subshell("docker", ["stop", "foo"]) } - it { expected_subshell("docker", ["rm", "-v", "foo"]) } + it { expected_subshell("docker", "stop foo") } + it { expected_subshell("docker", "rm -v foo") } end end @@ -71,18 +78,18 @@ context "when without arguments" do before { cli.start "ip".shellsplit } it do - expected_subshell("docker", array_including("inspect", "--format", /Networks.frontend.IPAddress/, "dnsdock")) + expected_subshell("docker", "inspect --format {{ .NetworkSettings.Networks.frontend.IPAddress }} dnsdock") end end context "when option `name` is present" do before { cli.start "ip --name foo".shellsplit } - it { expected_subshell("docker", array_including("foo")) } + it { expected_subshell("docker", "inspect --format {{ .NetworkSettings.Networks.frontend.IPAddress }} foo") } end context "when option `net` is present" do before { cli.start "ip --net foo".shellsplit } - it { expected_subshell("docker", array_including(/Networks.foo.IPAddress/)) } + it { expected_subshell("docker", "inspect --format {{ .NetworkSettings.Networks.foo.IPAddress }} dnsdock") } end end end diff --git a/spec/lib/dip/commands/nginx_spec.rb b/spec/lib/dip/commands/nginx_spec.rb index cfb38ea..3ae1cfc 100644 --- a/spec/lib/dip/commands/nginx_spec.rb +++ b/spec/lib/dip/commands/nginx_spec.rb @@ -14,52 +14,50 @@ it do expected_subshell( "docker", - ["run", "--detach", "--volume", "/var/run/docker.sock:/tmp/docker.sock:ro", - "--restart", "always", "--publish", "80:80", "--net", "frontend", "--name", "nginx", - "--label", "com.dnsdock.alias=docker", "bibendi/nginx-proxy:latest"] + "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest" ) end end context "when option `name` is present" do before { cli.start "up --name foo".shellsplit } - it { expected_subshell("docker", array_including("--name", "foo")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name foo --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } end context "when option `socket` is present" do before { cli.start "up --socket foo".shellsplit } - it { expected_subshell("docker", array_including("--volume", "foo:/tmp/docker.sock:ro")) } + it { expected_subshell("docker", "run --detach --volume foo:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } end context "when option `net` is present" do before { cli.start "up --net foo".shellsplit } it { expected_subshell("docker", ["network", "create", "foo"]) } - it { expected_subshell("docker", array_including("--net", "foo")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net foo --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } end context "when option `publish` is present" do before { cli.start "up --publish 80:80".shellsplit } - it { expected_subshell("docker", array_including("--publish", "80:80")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } context "when more than one port given" do before { cli.start "up --publish 80:80 443:443".shellsplit } - it { expected_subshell("docker", array_including("--publish", "80:80", "--publish", "443:443")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } end end context "when option `image` is present" do before { cli.start "up --image foo".shellsplit } - it { expected_subshell("docker", array_including("foo")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker foo") } end context "when option `domain` is present" do before { cli.start "up --domain foo".shellsplit } - it { expected_subshell("docker", array_including("com.dnsdock.alias=foo")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=foo bibendi/nginx-proxy:latest") } end context "when option `certs` is present" do before { cli.start "up --certs /home/whoami/certs_storage".shellsplit } - it { expected_subshell("docker", array_including("--volume", "/home/whoami/certs_storage:/etc/nginx/certs")) } + it { expected_subshell("docker", "run --detach --volume /var/run/docker.sock:/tmp/docker.sock:ro --volume /home/whoami/certs_storage:/etc/nginx/certs --restart always --publish 80:80 --net frontend --name nginx --label com.dnsdock.alias=docker bibendi/nginx-proxy:latest") } end end diff --git a/spec/lib/dip/commands/ssh_spec.rb b/spec/lib/dip/commands/ssh_spec.rb index 09b97e6..efc8a9c 100644 --- a/spec/lib/dip/commands/ssh_spec.rb +++ b/spec/lib/dip/commands/ssh_spec.rb @@ -13,28 +13,24 @@ before { cli.start "up".shellsplit } - it { expected_subshell("docker", array_including("volume", "create")) } - it { expected_subshell("docker", array_including("run", "--name=ssh-agent", "whilp/ssh-agent")) } - it do - expected_subshell("docker", - array_including("run", "--volume", "/user:/user", "--interactive", "--tty", - "whilp/ssh-agent", "ssh-add", "/user/.ssh/id_rsa")) - end + it { expected_subshell("docker", "volume create --name ssh_data") } + it { expected_subshell("docker", "run --detach --volume ssh_data:/ssh --name=ssh-agent whilp/ssh-agent") } + it { expected_subshell("docker", "run --rm --volume ssh_data:/ssh --volume /user:/user --interactive --tty whilp/ssh-agent ssh-add /user/.ssh/id_rsa") } end context "when option `key` is present" do before { cli.start "up --key /foo/bar-baz-rsa".shellsplit } - it { expected_subshell("docker", array_including("/foo/bar-baz-rsa")) } + it { expected_subshell("docker", "run --rm --volume ssh_data:/ssh --volume /root:/root --interactive --tty whilp/ssh-agent ssh-add /foo/bar-baz-rsa") } end context "when option `volume` is present" do before { cli.start "up --volume /foo/.ssh".shellsplit } - it { expected_subshell("docker", array_including("--volume", "/foo/.ssh:/foo/.ssh")) } + it { expected_subshell("docker", "run --rm --volume ssh_data:/ssh --volume /foo/.ssh:/foo/.ssh --interactive --tty whilp/ssh-agent ssh-add /root/.ssh/id_rsa") } end context "when option `user` is present" do before { cli.start "up -u 1000".shellsplit } - it { expected_subshell("docker", array_including("-u", "1000")) } + it { expected_subshell("docker", "run -u 1000 --detach --volume ssh_data:/ssh --name=ssh-agent whilp/ssh-agent") } end end @@ -49,6 +45,6 @@ describe Dip::Commands::SSH::Status do before { cli.start "status".shellsplit } - it { expected_subshell("docker", array_including("inspect", "ssh-agent")) } + it { expected_subshell("docker", "inspect --format {{.State.Status}} ssh-agent") } end end diff --git a/spec/support/shared_contexts/runner.rb b/spec/support/shared_contexts/runner.rb index 616090a..5edcd79 100644 --- a/spec/support/shared_contexts/runner.rb +++ b/spec/support/shared_contexts/runner.rb @@ -12,10 +12,12 @@ def expected_exec(cmd, argv, options = kind_of(Hash)) argv = Array(argv) if argv.is_a?(String) - expect(exec_runner).to have_received(:call).with(cmd, argv, options) + cmdline = [cmd, *argv].join(" ") + expect(exec_runner).to have_received(:call).with(cmdline, options) end def expected_subshell(cmd, argv, options = kind_of(Hash)) argv = Array(argv) if argv.is_a?(String) - expect(subshell_runner).to have_received(:call).with(cmd, argv, options) + cmdline = [cmd, *argv].join(" ") + expect(subshell_runner).to have_received(:call).with(cmdline, options) end