sonic-screwdriver 1.4.0 → 2.2.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 (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