sonic-screwdriver 1.4.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/bin/commit_docs.sh +26 -0
  3. data/.circleci/config.yml +72 -0
  4. data/.gitignore +2 -1
  5. data/CHANGELOG.md +29 -3
  6. data/Gemfile +3 -3
  7. data/Guardfile +17 -10
  8. data/LICENSE.txt +2 -2
  9. data/README.md +25 -28
  10. data/Rakefile +9 -2
  11. data/docs/_config.yml +3 -0
  12. data/docs/_docs/help.md +1 -1
  13. data/docs/_docs/install-bastion.md +5 -15
  14. data/docs/_docs/install.md +3 -13
  15. data/docs/_docs/next-steps.md +1 -1
  16. data/docs/_docs/settings.md +42 -56
  17. data/docs/_docs/tutorial-ecs-exec.md +16 -20
  18. data/docs/_docs/tutorial-ecs-sh.md +73 -0
  19. data/docs/_docs/tutorial-execute.md +106 -38
  20. data/docs/_docs/tutorial-ssh.md +15 -19
  21. data/docs/_docs/why-ec2-run-command.md +1 -1
  22. data/docs/_includes/commands.html +5 -5
  23. data/docs/_includes/content.html +5 -0
  24. data/docs/_includes/css/main.css +15 -9
  25. data/docs/_includes/css/sonic.css +7 -5
  26. data/docs/_includes/example.html +4 -4
  27. data/docs/_includes/footer.html +6 -4
  28. data/docs/_includes/reference.md +1 -0
  29. data/docs/_includes/subnav.html +2 -1
  30. data/docs/_reference/sonic-completion.md +44 -0
  31. data/docs/_reference/sonic-completion_script.md +25 -0
  32. data/docs/_reference/sonic-ecs-exec.md +30 -0
  33. data/docs/_reference/sonic-ecs-help.md +21 -0
  34. data/docs/_reference/sonic-ecs-sh.md +35 -0
  35. data/docs/_reference/sonic-ecs.md +25 -0
  36. data/docs/_reference/sonic-execute.md +85 -0
  37. data/docs/_reference/sonic-list.md +40 -0
  38. data/docs/_reference/sonic-ssh.md +86 -0
  39. data/docs/_reference/sonic-version.md +21 -0
  40. data/docs/bin/web +1 -1
  41. data/docs/img/tutorials/ec2-console-run-command.png +0 -0
  42. data/docs/quick-start.md +17 -22
  43. data/docs/reference.md +12 -0
  44. data/{bin → exe}/sonic +3 -3
  45. data/lib/bash_scripts/docker-exec.sh +1 -0
  46. data/lib/bash_scripts/docker-run.sh +8 -1
  47. data/lib/sonic.rb +11 -3
  48. data/lib/sonic/{aws_services.rb → aws_service.rb} +6 -1
  49. data/lib/sonic/base_command.rb +82 -0
  50. data/lib/sonic/checks.rb +2 -2
  51. data/lib/sonic/cli.rb +41 -29
  52. data/lib/sonic/command.rb +8 -22
  53. data/lib/sonic/completer.rb +161 -0
  54. data/lib/sonic/completer/script.rb +6 -0
  55. data/lib/sonic/completer/script.sh +10 -0
  56. data/lib/sonic/core.rb +15 -0
  57. data/lib/sonic/default/settings.yml +9 -16
  58. data/lib/sonic/docker.rb +30 -2
  59. data/lib/sonic/ecs.rb +22 -0
  60. data/lib/sonic/execute.rb +203 -51
  61. data/lib/sonic/help.rb +9 -0
  62. data/lib/sonic/help/command/send.md +10 -0
  63. data/lib/sonic/help/completion.md +22 -0
  64. data/lib/sonic/help/completion_script.md +3 -0
  65. data/lib/sonic/help/ecs/exec.md +8 -0
  66. data/lib/sonic/help/ecs/sh.md +13 -0
  67. data/lib/sonic/help/execute.md +59 -0
  68. data/lib/sonic/help/list.md +17 -0
  69. data/lib/sonic/help/ssh.md +60 -0
  70. data/lib/sonic/list.rb +5 -2
  71. data/lib/sonic/setting.rb +47 -0
  72. data/lib/sonic/ssh.rb +42 -23
  73. data/lib/sonic/ssh/identifier_detector.rb +7 -3
  74. data/lib/sonic/ui.rb +2 -2
  75. data/lib/sonic/version.rb +1 -1
  76. data/sonic.gemspec +14 -9
  77. data/spec/lib/cli_spec.rb +11 -11
  78. data/spec/lib/sonic/execute_spec.rb +1 -2
  79. data/spec/spec_helper.rb +18 -10
  80. metadata +115 -19
  81. data/Gemfile.lock +0 -134
  82. data/docs/_docs/tutorial-ecs-run.md +0 -100
  83. data/lib/sonic/cli/help.rb +0 -152
  84. data/lib/sonic/settings.rb +0 -115
data/lib/sonic/command.rb CHANGED
@@ -1,25 +1,11 @@
1
- require 'thor'
2
-
3
1
  module Sonic
4
- class Command < Thor
5
- class << self
6
- def dispatch(m, args, options, config)
7
- # Allow calling for help via:
8
- # sonic command help
9
- # sonic command -h
10
- # sonic command --help
11
- # sonic command -D
12
- #
13
- # as well thor's normal way:
14
- #
15
- # sonic help command
16
- help_flags = Thor::HELP_MAPPINGS + ["help"]
17
- if args.length > 1 && !(args & help_flags).empty?
18
- args -= help_flags
19
- args.insert(-2, "help")
20
- end
21
- super
22
- end
2
+ class Command < BaseCommand
3
+ desc "send [FILTER] [COMMAND]", "runs command across fleet of servers via AWS Run Command"
4
+ long_desc Help.text("command/send")
5
+ option :zero_warn, type: :boolean, default: true, desc: "Warns user when no instances found"
6
+ # filter - Filter ec2 instances by tag name or instance_ids separated by commas
7
+ def send(filter, *command)
8
+ Commander.new(command, options.merge(filter: filter)).execute
23
9
  end
24
10
  end
25
- end
11
+ end
@@ -0,0 +1,161 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ sonic completion
52
+ sonic completion hello
53
+ sonic completion hello name
54
+ sonic completion hello name --
55
+ sonic completion hello name --noop
56
+
57
+ sonic completion
58
+ sonic completion sub:goodbye
59
+ sonic completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module Sonic
72
+ class Completer
73
+ autoload :Script, 'sonic/completer/script'
74
+
75
+ def initialize(command_class, *params)
76
+ @params = params
77
+ @current_command = @params[0]
78
+ @command_class = command_class # CLI initiall
79
+ end
80
+
81
+ def run
82
+ if subcommand?(@current_command)
83
+ subcommand_class = @command_class.subcommand_classes[@current_command]
84
+ @params.shift # destructive
85
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
86
+ return
87
+ end
88
+
89
+ # full command has been found!
90
+ unless found?(@current_command)
91
+ puts all_commands
92
+ return
93
+ end
94
+
95
+ # will only get to here if command aws found (above)
96
+ arity = @command_class.instance_method(@current_command).arity.abs
97
+ if @params.size > arity or thor_group_command?
98
+ puts options_completion
99
+ else
100
+ puts params_completion
101
+ end
102
+ end
103
+
104
+ def subcommand?(command)
105
+ @command_class.subcommands.include?(command)
106
+ end
107
+
108
+ # hacky way to detect that command is a registered Thor::Group command
109
+ def thor_group_command?
110
+ command_params(raw=true) == [[:rest, :args]]
111
+ end
112
+
113
+ def found?(command)
114
+ public_methods = @command_class.public_instance_methods(false)
115
+ command && public_methods.include?(command.to_sym)
116
+ end
117
+
118
+ # all top-level commands
119
+ def all_commands
120
+ commands = @command_class.all_commands.reject do |k,v|
121
+ v.is_a?(Thor::HiddenCommand)
122
+ end
123
+ commands.keys
124
+ end
125
+
126
+ def command_params(raw=false)
127
+ params = @command_class.instance_method(@current_command).parameters
128
+ # Example:
129
+ # >> Sub.instance_method(:goodbye).parameters
130
+ # => [[:req, :name]]
131
+ # >>
132
+ raw ? params : params.map!(&:last)
133
+ end
134
+
135
+ def params_completion
136
+ offset = @params.size - 1
137
+ offset_params = command_params[offset..-1]
138
+ command_params[offset..-1].first
139
+ end
140
+
141
+ def options_completion
142
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
143
+
144
+ method_options = @command_class.all_commands[@current_command].options.keys
145
+ class_options = @command_class.class_options.keys
146
+
147
+ all_options = method_options + class_options + ['help']
148
+
149
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
150
+ filtered_options = all_options - used
151
+ filtered_options.uniq
152
+ end
153
+
154
+ # Useful for debugging. Using puts messes up completion.
155
+ def log(msg)
156
+ File.open("/tmp/complete.log", "a") do |file|
157
+ file.puts(msg)
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,6 @@
1
+ class Sonic::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _sonic() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(sonic completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _sonic sonic
data/lib/sonic/core.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Sonic
5
+ module Core
6
+ def root
7
+ path = ENV['SONIC_ROOT'] || '.'
8
+ Pathname.new(path)
9
+ end
10
+
11
+ def profile
12
+ ENV['SONIC_PROFILE'] || ENV['AWS_PROFILE']
13
+ end
14
+ end
15
+ end
@@ -1,16 +1,9 @@
1
- bastion: # cluster_host mapping
2
- default: # default is nil - which means a bastion host wont be used
3
- # Examples:
4
- # prod: bastion.mydomain.com
5
- # stag: ubuntu@bastion-stag.mydomain.com
6
- host_key_check: false
7
- service_cluster:
8
- default: # defaults to nil
9
- # Examples:
10
- # hi-web-prod: prod
11
- # hi-clock-prod: prod
12
- # hi-worker-prod: prod
13
- # hi-web-stag: stag
14
- # hi-clock-stag: stag
15
- # hi-worker-stag: stag
16
- user: ec2-user
1
+ bastion:
2
+ host_key_check: false
3
+ # host:
4
+
5
+ ssh:
6
+ user: ec2-user
7
+
8
+ ecs_service_cluster_map:
9
+ default: default
data/lib/sonic/docker.rb CHANGED
@@ -19,6 +19,7 @@ module Sonic
19
19
 
20
20
  def setup
21
21
  validate!
22
+ confirm_ssh_access
22
23
  copy_over_container_data
23
24
  end
24
25
 
@@ -29,7 +30,7 @@ module Sonic
29
30
 
30
31
  # command is an Array
31
32
  def execute(*command)
32
- UI.say "=> #{command.join(' ')}".colorize(:green)
33
+ UI.say "=> #{command.join(' ')}".color(:green)
33
34
  success = system(*command)
34
35
  unless success
35
36
  UI.error(command.join(' '))
@@ -46,10 +47,37 @@ module Sonic
46
47
  kernel_exec(*args)
47
48
  end
48
49
 
50
+ def build_host
51
+ host = @bastion ? bastion_host : ssh_host
52
+ host = "#{@user}@#{host}" unless host.include?('@')
53
+ host
54
+ end
55
+
56
+ def confirm_ssh_access
57
+ host = build_host
58
+ puts "Checking access to instance #{detector.instance_id}"
59
+
60
+ ssh = ["ssh", ssh_options, "-At", host, "uptime", "2>&1"]
61
+ command = ssh.join(' ')
62
+ puts "=> #{command}".color(:green)
63
+ output = `#{command}`
64
+ if output.include?("Permission denied")
65
+ puts output
66
+ UI.error("Access to the instance denied. Maybe check your ssh keys.")
67
+ exit 1
68
+ elsif output.include?(" up ")
69
+ puts "Access OK!"
70
+ else
71
+ puts output
72
+ UI.error("There was an error trying to access the instnace.")
73
+ exit 1
74
+ end
75
+ end
76
+
49
77
  def copy_over_container_data
50
78
  create_container_data
51
79
 
52
- host = @bastion ? bastion_host : ssh_host
80
+ host = build_host
53
81
 
54
82
  # LEVEL 1
55
83
  # Always clean up remote /tmp/sonic in case of previous interrupted run.
data/lib/sonic/ecs.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Sonic
2
+ autoload :Docker, 'sonic/docker'
3
+
4
+ class Ecs < BaseCommand
5
+
6
+ class_option :bastion, desc: "Bastion jump host to use. Defaults to no bastion server."
7
+ class_option :cluster, desc: "ECS Cluster to use. Default cluster is default"
8
+
9
+ desc "exec [ECS_SERVICE]", "docker exec into running docker container associated with the service on a container instance"
10
+ long_desc Help.text("ecs/exec")
11
+ def exec(service, *command)
12
+ Docker.new(service, options.merge(command: command)).exec
13
+ end
14
+
15
+ # Cannot name the command run because that is a reserved Thor keyword :(
16
+ desc "sh [ECS_SERVICE]", "docker run with the service on a container instance"
17
+ long_desc Help.text("ecs/sh")
18
+ def sh(service, *command)
19
+ Docker.new(service, options.merge(command: command)).run
20
+ end
21
+ end
22
+ end
data/lib/sonic/execute.rb CHANGED
@@ -1,11 +1,15 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash'
3
+
1
4
  module Sonic
2
5
  class Execute
3
- include AwsServices
6
+ include AwsService
4
7
 
5
8
  def initialize(command, options)
6
9
  @command = command
7
10
  @options = options
8
- @filter = @options[:filter].split(',').map{|s| s.strip}
11
+ @tags = @options[:tags]
12
+ @instance_ids = @options[:instance_ids]
9
13
  end
10
14
 
11
15
  # aws ssm send-command \
@@ -15,40 +19,193 @@ module Sonic
15
19
  # --parameters '{"commands":["#!/usr/bin/python","print \"Hello world from python\""]}' \
16
20
  # --query "Command.CommandId"
17
21
  def execute
22
+ check_filter_options!
18
23
  ssm_options = build_ssm_options
19
24
  if @options[:noop]
20
25
  UI.noop = true
21
- command_id = "fake command id"
26
+ command_id = "fake command id for noop mode"
22
27
  success = true # fake it for specs
23
28
  else
24
29
  instances_count = check_instances
25
30
  return unless instances_count > 0
26
31
 
27
32
  success = nil
33
+ puts "Sending command to SSM with options:"
34
+ puts YAML.dump(ssm_options.deep_stringify_keys)
35
+ puts
28
36
  begin
29
- resp = ssm.send_command(ssm_options)
37
+ resp = send_command(ssm_options)
38
+
30
39
  command_id = resp.command.command_id
31
40
  success = true
32
41
  rescue Aws::SSM::Errors::InvalidInstanceId => e
33
42
  ssm_invalid_instance_error_message(e)
34
43
  end
35
44
  end
36
- if success
37
- UI.say "Command sent to AWS SSM. To check the details of the command:"
38
- display_list_command(command_id)
45
+
46
+ return unless success
47
+
48
+ # IF COMMAND IS ONLY ON A SINGLE INSTANCE THEN WILL DISPLAY A BUNCH OF
49
+ # INFO ON THE INSTANCE. IF ITS A LOT OF INSTANCES, THEN SHOW A SUMMARY
50
+ # OF COMMANDS THAT WILL LEAD TO THE OUTPUT OF EACH INSTANCE.
51
+ UI.say "Command sent to AWS SSM. To check the details of the command:"
52
+ display_ssm_commands(command_id, ssm_options)
53
+ puts
54
+ return if @options[:noop]
55
+ status = wait(command_id)
56
+ instances_found = display_ssm_output(command_id)
57
+ display_console_url(command_id)
58
+
59
+ if status == "Success"
60
+ puts "Command successful: #{status}".color(:green)
61
+ exit(0)
62
+ else
63
+ puts "Command unsuccessful: #{status}".color(:red)
64
+ exit(1)
65
+ end
66
+ end
67
+
68
+ def wait(command_id)
69
+ ongoing_states = ["Pending", "InProgress", "Delayed"]
70
+
71
+ print "Waiting for ssm command to finish..."
72
+ resp = ssm.list_commands(command_id: command_id)
73
+ status = resp["commands"].first["status"]
74
+ while ongoing_states.include?(status)
75
+ resp = ssm.list_commands(command_id: command_id)
76
+ status = resp["commands"].first["status"]
77
+ sleep 1
78
+ print '.'
79
+ end
80
+ puts "\nCommand finished."
81
+ puts
82
+ status
83
+ end
84
+
85
+ def display_ssm_output(command_id)
86
+ resp = ssm.list_command_invocations(command_id: command_id)
87
+ command_invocations = resp.command_invocations
88
+ command_invocation = command_invocations.first
89
+ unless command_invocation
90
+ puts "WARN: No instances found that matches the --tags or --instance-ids option".color(:yellow)
91
+ return false # instances_found
92
+ end
93
+ instance_id = command_invocation.instance_id
94
+
95
+ if command_invocations.size > 1
96
+ puts "Multiple instance targets. Total targets: #{command_invocations.size}. Only displaying output for #{instance_id}."
97
+ else
98
+ puts "Displaying output for #{instance_id}."
99
+ end
100
+
101
+ resp = ssm.get_command_invocation(
102
+ command_id: command_id, instance_id: instance_id
103
+ )
104
+ puts "Command status: #{colorized_status(resp["status"])}"
105
+ ssm_output(resp, "output")
106
+ ssm_output(resp, "error")
107
+ puts
108
+ true # instances_found
109
+ end
110
+
111
+ def display_console_url(command_id)
112
+ region = `aws configure get region`.strip rescue 'us-east-1'
113
+ console_url = "https://#{region}.console.aws.amazon.com/systems-manager/run-command/#{command_id}"
114
+ puts "To see the more output details visit:"
115
+ puts " #{console_url}"
116
+ puts
117
+ copy_paste_clipboard(console_url, "Pro tip: the console url is already in your copy/paste clipboard.")
118
+ end
119
+
120
+ def colorized_status(status)
121
+ case status
122
+ when "Success"
123
+ status.color(:green)
124
+ when "Failed"
125
+ status.color(:red)
126
+ else
127
+ status
39
128
  end
40
129
  end
41
130
 
131
+ # type: output or error
132
+ def ssm_output(resp, type)
133
+ content_key = "standard_#{type}_content"
134
+ s3_key = "standard_#{type}_url"
135
+
136
+ content = resp[content_key]
137
+ return if content.empty?
138
+
139
+ puts "Command standard #{type}:"
140
+ # "https://s3.amazonaws.com/infra-prod/ssm/commands/sonic/0a4f4bef-8f63-4235-8b30-ae296477261a/i-0b2e6e187a3f9ada9/awsrunPowerShellScript/0.awsrunPowerShellScript/stderr">
141
+ if content.include?("--output truncated--") && !resp[s3_key].empty?
142
+ s3_url = resp[s3_key]
143
+ info = s3_url.sub('https://s3.amazonaws.com/', '').split('/')
144
+ bucket = info[0]
145
+ key = info[1..-1].join('/')
146
+ resp = s3.get_object(bucket: bucket, key: key)
147
+ data = resp.body.read
148
+ puts data
149
+
150
+ path = "/tmp/sonic-output.txt"
151
+ puts "------"
152
+ puts "Output also written to #{path}"
153
+ IO.write(path, data)
154
+ else
155
+ puts content
156
+ end
157
+
158
+ # puts "#{s3_key}: #{resp[s3_key]}"
159
+ end
160
+
161
+ def send_command(options)
162
+ retries = 0
163
+
164
+ begin
165
+ resp = ssm.send_command(options)
166
+ rescue Aws::SSM::Errors::UnsupportedPlatformType
167
+ retries += 1
168
+ # toggle AWS-RunShellScript / AWS-RunPowerShellScript
169
+ options[:document_name] =
170
+ options[:document_name] == "AWS-RunShellScript" ?
171
+ "AWS-RunPowerShellScript" : "AWS-RunShellScript"
172
+
173
+ puts "#{$!}"
174
+ puts "Retrying with document_name #{options[:document_name]}"
175
+ puts "Retries: #{retries}"
176
+
177
+ retries <= 1 ? retry : raise
178
+ end
179
+
180
+ resp
181
+ end
182
+
42
183
  def build_ssm_options
43
- criteria = transform_filter(@filter)
184
+ criteria = transform_filter_option
44
185
  command = build_command(@command)
45
- criteria.merge(
46
- document_name: "AWS-RunShellScript",
47
- comment: "sonic #{ARGV.join(' ')}",
48
- parameters: {
49
- "commands" => command
50
- }
186
+ options = criteria.merge(
187
+ document_name: "AWS-RunShellScript", # default
188
+ comment: "sonic #{ARGV.join(' ')}"[0..99], # comment has a max of 100 chars
189
+ parameters: { "commands" => command },
190
+ # Default CloudWatchLog settings. Can be overwritten with settings.yml send_command
191
+ # IMPORTANT: make sure the EC2 instance the command runs on has access to write to CloudWatch Logs.
192
+ cloud_watch_output_config: {
193
+ # cloud_watch_log_group_name: "ssm", # Defaults to /aws/ssm/AWS-RunShellScript (aws/ssm/SystemsManagerDocumentName https://amzn.to/38TKVse)
194
+ cloud_watch_output_enabled: true,
195
+ },
51
196
  )
197
+ settings_options = settings["send_command"] || {}
198
+ options.merge(settings_options.deep_symbolize_keys)
199
+ end
200
+
201
+ def settings
202
+ @settings ||= Setting.new.data
203
+ end
204
+
205
+ def check_filter_options!
206
+ return if @tags || @instance_ids
207
+ puts "ERROR: Please provide --tags or --instance-ids option".color(:red)
208
+ exit 1
52
209
  end
53
210
 
54
211
  #
@@ -58,41 +215,31 @@ module Sonic
58
215
  #
59
216
  # Examples
60
217
  #
61
- # transform_filter(["hi-web-prod", "hi-worker-prod", "i-006a097bb10643e20"])
218
+ # transform_filter_option
62
219
  # # => {
63
220
  # instance_ids: ["i-006a097bb10643e20"],
64
221
  # targets: [{key: "Name", values: "hi-web-prod,hi-worker-prod"}]
65
222
  # }
66
223
  #
67
224
  # Returns the duplicated String.
68
- def transform_filter(filter)
69
- valid = validate_filter(filter)
70
- unless valid
71
- UI.error("The filter you provided '#{filter.join(',')}' is not valid.")
72
- UI.say("The filter must either be all instance ids or just a list of tag names.")
73
- exit 1
74
- end
75
-
76
- if filter.detect { |i| instance_id?(i) }
77
- instance_ids = filter
78
- {instance_ids: instance_ids}
79
- else
80
- tags = filter
81
- targets = [{
82
- key: "tag:#{tag_name}",
83
- values: tags
84
- }]
225
+ def transform_filter_option
226
+ if @tags
227
+ list = @tags.split(';')
228
+ targets = list.inject([]) do |final,item|
229
+ tag_name,value_list = item.split('=')
230
+ values = value_list.split(',').map(&:strip)
231
+ # structure expected by ssm send_command
232
+ option = {
233
+ key: "tag:#{tag_name}",
234
+ values: values
235
+ }
236
+ final << option
237
+ final
238
+ end
85
239
  {targets: targets}
86
- end
87
- end
88
-
89
- # Either all instance ids are no instance ids is a valid filter
90
- def validate_filter(filter)
91
- if filter.detect { |i| instance_id?(i) }
92
- instance_ids = filter.select { |i| instance_id?(i) }
93
- instance_ids.size == filter.size
94
- else
95
- true
240
+ else # @instance_ids
241
+ instance_ids = @instance_ids.split(',')
242
+ {instance_ids: instance_ids}
96
243
  end
97
244
  end
98
245
 
@@ -109,7 +256,7 @@ module Sonic
109
256
  # The script is being feed inline so just join the command together into one script.
110
257
  # Still keep in an array form because that's how ssn.send_command works with AWS-RunShellScript
111
258
  # usually reads the command.
112
- [command.join(" ")]
259
+ command.is_a?(Array) ? command : [command]
113
260
  end
114
261
  end
115
262
 
@@ -124,7 +271,7 @@ You can use the following command to check registered instances to SSM.
124
271
  #{ssm_describe_command}
125
272
  EOS
126
273
  UI.warn(message)
127
- copy_paste_clipboard(ssm_describe_command)
274
+ copy_paste_clipboard(ssm_describe_command, "Pro tip: ssm describe-instance-information already in your copy/paste clipboard.")
128
275
  end
129
276
 
130
277
  def file_path?(command)
@@ -136,7 +283,7 @@ You can use the following command to check registered instances to SSM.
136
283
  def file_path(command)
137
284
  path = command.first
138
285
  path = path.sub('file://', '')
139
- path = "#{@options[:project_root]}/#{path}" if @options[:project_root]
286
+ path = "#{Sonic.root}/#{path}"
140
287
  path
141
288
  end
142
289
 
@@ -150,7 +297,7 @@ You can use the following command to check registered instances to SSM.
150
297
  instances = List.new(@options).instances
151
298
  if instances.count == 0
152
299
  message = <<-EOL
153
- Unable to find any instances with filter #{@filter.join(',')}.
300
+ Unable to find any instances with filter #{@filter.join(',')}.
154
301
  Are you sure you specify the filter with either a EC2 tag or list instance ids?
155
302
  If you are using ECS identifiers, they are not supported with this command.
156
303
  EOL
@@ -170,16 +317,21 @@ EOL
170
317
  text =~ /i-.{17}/ || text =~ /i-.{8}/
171
318
  end
172
319
 
173
- def display_list_command(command_id)
174
- list_command = "aws ssm list-commands --command-id #{command_id}"
320
+ def display_ssm_commands(command_id, ssm_options)
321
+ list_command = " aws ssm list-commands --command-id #{command_id}"
175
322
  UI.say list_command
176
- copy_paste_clipboard(list_command)
323
+
324
+ return unless ssm_options[:instance_ids]
325
+ ssm_options[:instance_ids].each do |instance_id|
326
+ get_command = " aws ssm get-command-invocation --command-id #{command_id} --instance-id #{instance_id}"
327
+ UI.say get_command
328
+ end
177
329
  end
178
330
 
179
- def copy_paste_clipboard(command)
331
+ def copy_paste_clipboard(command, text)
180
332
  return unless RUBY_PLATFORM =~ /darwin/
181
333
  system("echo '#{command}' | pbcopy")
182
- UI.say "Pro tip: the aws ssm command is already in your copy/paste clipboard."
334
+ UI.say text
183
335
  end
184
336
  end
185
337
  end