stax 0.0.3 → 0.0.4

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.
@@ -40,7 +40,9 @@ module Stax
40
40
  def tables
41
41
  print_table stack_tables.map { |r|
42
42
  t = Aws::DynamoDB.table(r.physical_resource_id)
43
- [ t.table_name, color(t.table_status, COLORS), t.item_count, t.table_size_bytes, t.creation_date_time ]
43
+ g = Aws::DynamoDB.global_table(r.physical_resource_id)
44
+ regions = g.nil? ? '-' : g.replication_group.map(&:region_name).sort.join(',')
45
+ [ t.table_name, color(t.table_status, COLORS), t.item_count, t.table_size_bytes, t.creation_date_time, regions ]
44
46
  }
45
47
  end
46
48
 
@@ -94,6 +96,20 @@ module Stax
94
96
  )
95
97
  end
96
98
 
99
+ desc 'put ID', 'put items from stdin to table'
100
+ method_option :verbose, aliases: '-v', type: :boolean, default: false, desc: 'show progress'
101
+ def put(id)
102
+ table = my.resource(id)
103
+ count = 0
104
+ $stdin.each do |line|
105
+ Aws::DynamoDB.put(table_name: table, item: JSON.parse(line))
106
+ print '.' if options[:verbose]
107
+ count += 1
108
+ end
109
+ print "\n" if options[:verbose]
110
+ puts "put #{count} items to #{table}"
111
+ end
112
+
97
113
  end
98
114
  end
99
115
  end
@@ -11,9 +11,14 @@ module Stax
11
11
  module Cmd
12
12
  class Ec2 < SubCommand
13
13
  COLORS = {
14
+ ## instances
14
15
  running: :green,
15
16
  stopped: :yellow,
16
17
  terminated: :red,
18
+ ## images
19
+ available: :green,
20
+ pending: :yellow,
21
+ failed: :red,
17
22
  }
18
23
 
19
24
  desc 'ls', 'list instances for stack'
@@ -31,7 +36,7 @@ module Stax
31
36
  ]
32
37
  }
33
38
  end
34
- end
35
39
 
40
+ end
36
41
  end
37
42
  end
@@ -0,0 +1,68 @@
1
+ require 'base64'
2
+ require 'stax/aws/ecr'
3
+
4
+ module Stax
5
+ module Ecr
6
+ def self.included(thor)
7
+ thor.desc(:ecr, 'ECR subcommands')
8
+ thor.subcommand(:ecr, Cmd::Ecr)
9
+ end
10
+
11
+ def ecr_registry
12
+ @_ecr_registry ||= "#{aws_account_id}.dkr.ecr.#{aws_region}.amazonaws.com"
13
+ end
14
+
15
+ def ecr_repositories
16
+ @_ecr_repositories ||= Aws::Cfn.resources_by_type(stack_name, 'AWS::ECR::Repository')
17
+ end
18
+
19
+ def ecr_repository_names
20
+ @_ecr_repository_names ||= ecr_repositories.map(&:physical_resource_id)
21
+ end
22
+
23
+ ## override to set an explicit repo name
24
+ def ecr_repository_name
25
+ @_ecr_repository_name ||= (ecr_repository_names&.first || app_name)
26
+ end
27
+ end
28
+
29
+ module Cmd
30
+ class Ecr < SubCommand
31
+
32
+ desc 'registry', 'show ECR registry'
33
+ def registry
34
+ puts my.ecr_registry
35
+ end
36
+
37
+ ## TODO: reimplement using --password-stdin
38
+ desc 'login', 'login to ECR registry'
39
+ def login
40
+ Aws::Ecr.auth.each do |auth|
41
+ debug("Login to ECR registry #{auth.proxy_endpoint}")
42
+ user, pass = Base64.decode64(auth.authorization_token).split(':')
43
+ system "docker login -u #{user} -p #{pass} #{auth.proxy_endpoint}"
44
+ end
45
+ end
46
+
47
+ desc 'repositories', 'list ECR repositories'
48
+ def repositories
49
+ print_table Aws::Ecr.repositories(repository_names: my.ecr_repository_names).map { |r|
50
+ [r.repository_name, r.repository_uri, r.created_at]
51
+ }
52
+ end
53
+
54
+ desc 'images', 'list ECR images'
55
+ method_option :repositories, aliases: '-r', type: :array, default: nil, desc: 'list of repos'
56
+ def images
57
+ (options[:repositories] || my.ecr_repository_names).each do |repo|
58
+ debug("Images in repo #{repo}")
59
+ print_table Aws::Ecr.images(repository_name: repo).map { |i|
60
+ [i.image_tags.join(' '), i.image_digest, human_bytes(i.image_size_in_bytes), i.image_pushed_at]
61
+ }
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+
68
+ end
@@ -1,4 +1,5 @@
1
1
  require 'stax/aws/ecs'
2
+ require_relative 'ecs/deploy'
2
3
 
3
4
  module Stax
4
5
  module Ecs
@@ -7,8 +8,12 @@ module Stax
7
8
  thor.subcommand(:ecs, Cmd::Ecs)
8
9
  end
9
10
 
11
+ def ecs_clusters
12
+ @_ecs_clusters ||= Aws::Cfn.resources_by_type(stack_name, 'AWS::ECS::Cluster')
13
+ end
14
+
10
15
  def ecs_cluster_name
11
- 'default'
16
+ @_ecs_cluster_name ||= (ecs_clusters&.first&.physical_resource_id || 'default')
12
17
  end
13
18
 
14
19
  def ecs_services
@@ -19,11 +24,22 @@ module Stax
19
24
  @_ecs_task_definitions ||= Aws::Cfn.resources_by_type(stack_name, 'AWS::ECS::TaskDefinition')
20
25
  end
21
26
 
27
+ ## mangle taskdef arn into family name
28
+ def ecs_task_families
29
+ ecs_task_definitions.map do |r|
30
+ r.physical_resource_id.split(':')[-2].split('/').last
31
+ end
32
+ end
33
+
34
+ def ecs_service_names
35
+ @_ecs_service_names ||= ecs_services.map(&:physical_resource_id)
36
+ end
37
+
22
38
  def ecs_service_objects
23
- Aws::Ecs.services(ecs_cluster_name, ecs_services.map(&:physical_resource_id))
39
+ Aws::Ecs.services(ecs_cluster_name, ecs_service_names)
24
40
  end
25
41
 
26
- ## register a new revision of existing task definition
42
+ ## deprecated: register a new revision of existing task definition
27
43
  def ecs_update_taskdef(id)
28
44
  taskdef = Aws::Ecs.task_definition(resource(id))
29
45
  debug("Registering new revision of #{taskdef.family}")
@@ -33,7 +49,7 @@ module Stax
33
49
  end
34
50
  end
35
51
 
36
- ## update service to use a new task definition
52
+ ## deprecated: update service to use a new task definition
37
53
  def ecs_update_service(id, taskdef)
38
54
  service_name = resource(id).split('/').last
39
55
  taskdef_name = taskdef.task_definition_arn.split('/').last
@@ -42,6 +58,7 @@ module Stax
42
58
  puts s.task_definition
43
59
  end
44
60
  end
61
+
45
62
  end
46
63
 
47
64
  module Cmd
@@ -57,6 +74,10 @@ module Stax
57
74
  def ecs_task_definition(id)
58
75
  Aws::Cfn.id(my.stack_name, id)
59
76
  end
77
+
78
+ def print_event(e)
79
+ puts "#{set_color(e.created_at, :green)} #{e.message}"
80
+ end
60
81
  end
61
82
 
62
83
  desc 'clusters', 'ECS cluster for stack'
@@ -84,9 +105,26 @@ module Stax
84
105
  def events
85
106
  my.ecs_service_objects.each do |s|
86
107
  debug("Events for #{s.service_name}")
87
- print_table s.events.first(options[:number]).map { |e|
88
- [set_color(e.created_at, :green), e.message]
89
- }.reverse
108
+ s.events.first(options[:number]).reverse.map(&method(:print_event))
109
+ end
110
+ end
111
+
112
+ desc 'tail [SERVICE]', 'tail ECS events'
113
+ def tail(service = nil)
114
+ trap('SIGINT', 'EXIT') # clean exit with ctrl-c
115
+ service ||= my.ecs_service_names.first
116
+ latest_event = Aws::Ecs.services(my.ecs_cluster_name, [service]).first.events.first
117
+ print_event(latest_event)
118
+ last_seen = latest_event.id
119
+ loop do
120
+ sleep 5
121
+ unseen = []
122
+ Aws::Ecs.services(my.ecs_cluster_name, [service]).first.events.each do |e|
123
+ break if e.id == last_seen
124
+ unseen.unshift(e)
125
+ end
126
+ unseen.each(&method(:print_event))
127
+ last_seen = unseen.last.id unless unseen.empty?
90
128
  end
91
129
  end
92
130
 
@@ -109,39 +147,65 @@ module Stax
109
147
  }
110
148
  end
111
149
 
150
+ desc 'env', 'env vars for latest rev of task families'
151
+ def env
152
+ my.ecs_task_families.each do |family|
153
+ Aws::Ecs.task_definition(family).container_definitions.each do |c|
154
+ debug("Environment for #{family} #{c.name}")
155
+ print_table c.environment.map { |e|
156
+ [e.name, e.value]
157
+ }
158
+ end
159
+ end
160
+ end
161
+
112
162
  desc 'tasks', 'ECS tasks for stack'
113
163
  method_option :status, aliases: '-s', type: :string, default: 'RUNNING', desc: 'status to list'
114
164
  def tasks
115
- print_table Aws::Ecs.tasks(my.ecs_cluster_name, options[:status].upcase).map { |t|
116
- [
117
- t.task_arn.split('/').last,
118
- t.task_definition_arn.split('/').last,
119
- t.container_instance_arn&.split('/')&.last || '--',
120
- color(t.last_status, COLORS),
121
- "(#{t.desired_status})",
122
- t.started_by,
123
- ]
124
- }
165
+ my.ecs_services.each do |s|
166
+ name = s.physical_resource_id.split('/').last
167
+ debug("Tasks for service #{name}")
168
+ Aws::Ecs.tasks(
169
+ cluster: my.ecs_cluster_name,
170
+ service_name: s.physical_resource_id,
171
+ desired_status: options[:status].upcase,
172
+ ).map { |t|
173
+ [
174
+ t.task_arn.split('/').last,
175
+ t.task_definition_arn.split('/').last,
176
+ t.container_instance_arn&.split('/')&.last || '--',
177
+ color(t.last_status, COLORS),
178
+ "(#{t.desired_status})",
179
+ t.started_by,
180
+ ]
181
+ }.tap(&method(:print_table))
182
+ end
125
183
  end
126
184
 
127
185
  desc 'containers', 'containers for running tasks'
128
186
  method_option :status, aliases: '-s', type: :string, default: 'RUNNING', desc: 'status to list'
129
187
  def containers
130
- debug("Containers for cluster #{my.ecs_cluster_name}")
131
- print_table Aws::Ecs.tasks(my.ecs_cluster_name, options[:status].upcase).map { |task|
132
- task_defn = task.task_definition_arn.split('/').last
133
- task.containers.map { |c|
134
- [
135
- c.container_arn.split('/').last,
136
- c.name,
137
- color(c.last_status, COLORS),
138
- c.network_interfaces.map(&:private_ipv_4_address).join(','),
139
- task_defn,
140
- c.exit_code,
141
- c.reason,
142
- ]
143
- }
144
- }.flatten(1)
188
+ my.ecs_services.each do |s|
189
+ Aws::Ecs.tasks(
190
+ cluster: my.ecs_cluster_name,
191
+ service_name: s.physical_resource_id,
192
+ desired_status: options[:status].upcase,
193
+ ).each do |t|
194
+ task = t.task_arn.split('/').last
195
+ debug("Containers for task #{task}")
196
+ print_table t.containers.map { |c|
197
+ [
198
+ c.name,
199
+ c.container_arn.split('/').last,
200
+ color(c.last_status, COLORS),
201
+ c.network_interfaces.map(&:private_ipv_4_address).join(','),
202
+ t.task_definition_arn.split('/').last,
203
+ c.exit_code,
204
+ c.reason,
205
+ ]
206
+ }
207
+ end
208
+ end
145
209
  end
146
210
 
147
211
  desc 'instances', 'ECS instances'
@@ -178,8 +242,9 @@ module Stax
178
242
  method_option :desired, aliases: '-d', type: :numeric, default: nil, desc: 'desired container count'
179
243
  def scale
180
244
  my.ecs_services.each do |s|
181
- debug("Scaling service #{s.logical_resource_id}")
245
+ debug("Scaling service #{s.physical_resource_id.split('/').last}")
182
246
  Aws::Ecs.update_service(
247
+ cluster: my.ecs_cluster_name,
183
248
  service: s.physical_resource_id,
184
249
  desired_count: options[:desired],
185
250
  ).tap do |s|
@@ -0,0 +1,49 @@
1
+ module Stax
2
+ module Ecs
3
+
4
+ ## convert to hash for registering new taskdef
5
+ def taskdef_to_hash(taskdef)
6
+ args = %i[family cpu memory requires_compatibilities task_role_arn execution_role_arn network_mode container_definitions volumes placement_constraints]
7
+ taskdef.to_hash.slice(*args)
8
+ end
9
+
10
+ def get_taskdef(service)
11
+ debug("Current task definition for #{service.service_name}")
12
+ Aws::Ecs.task_definition(service.task_definition).tap do |t|
13
+ puts t.task_definition_arn
14
+ end
15
+ end
16
+
17
+ def register_taskdef(hash)
18
+ debug("Registering new revision")
19
+ Aws::Ecs.client.register_task_definition(hash).task_definition.tap do |t|
20
+ puts t.task_definition_arn
21
+ end
22
+ end
23
+
24
+ def update_service(service, taskdef)
25
+ debug("Updating #{service.service_name} to new revision")
26
+ Aws::Ecs.update_service(
27
+ service: service.service_name,
28
+ task_definition: taskdef.task_definition_arn
29
+ ).tap do |s|
30
+ puts s.deployments.first.id
31
+ end
32
+ end
33
+
34
+ ## update taskdef for a service, triggering a deploy
35
+ ## modify current taskdef in block
36
+ def ecs_deploy(id, &block)
37
+ service = Aws::Ecs.services(ecs_cluster_name, [resource(id)]).first
38
+ taskdef = get_taskdef(service)
39
+
40
+ ## convert to a hash and modify in block
41
+ hash = taskdef_to_hash(taskdef)
42
+ yield(hash) if block_given?
43
+
44
+ taskdef = register_taskdef(hash)
45
+ update_service(service, taskdef)
46
+ end
47
+
48
+ end
49
+ end
@@ -62,12 +62,12 @@ module Stax
62
62
  end
63
63
  end
64
64
 
65
- desc 'tail [STREAM]', 'tail latest/given log stream'
65
+ desc 'events [STREAM]', 'show events just from latest/given log stream'
66
66
  method_option :group, aliases: '-g', type: :string, default: nil, desc: 'log group to tail'
67
67
  method_option :numlines, aliases: '-n', type: :numeric, default: 10, desc: 'number of lines to show'
68
68
  method_option :follow, aliases: '-f', type: :boolean, default: false, desc: 'follow log output'
69
69
  method_option :sleep, aliases: '-s', type: :numeric, default: 1, desc: 'seconds to sleep between poll for new data'
70
- def tail(stream = nil)
70
+ def events(stream = nil)
71
71
  trap('SIGINT', 'EXIT') # clean exit with ctrl-c
72
72
  group = ((g = options[:group]) ? log_groups[g] : log_groups.values.first).log_group_name
73
73
  stream ||= latest_stream(group).log_stream_name
@@ -89,6 +89,77 @@ module Stax
89
89
  end
90
90
  end
91
91
 
92
+ desc 'tail', 'tail all events from log group'
93
+ method_option :group, aliases: '-g', type: :string, default: nil, desc: 'log group to tail'
94
+ method_option :streams, aliases: '-s', type: :array, default: nil, desc: 'limit to given streams'
95
+ def tail
96
+ trap('SIGINT', 'EXIT') # clean exit with ctrl-c
97
+ group = ((g = options[:group]) ? log_groups[g] : log_groups.values.first).log_group_name
98
+
99
+ debug("Log group #{group}")
100
+ token = nil
101
+ start_time = Time.now.to_i - 30 # start 30 sec ago
102
+
103
+ loop do
104
+ end_time = Time.now.to_i
105
+ resp = Aws::Logs.client.filter_log_events(
106
+ log_group_name: group,
107
+ log_stream_names: options[:streams],
108
+ start_time: start_time * 1000, # aws needs msec
109
+ end_time: end_time * 1000,
110
+ next_token: token,
111
+ interleaved: true,
112
+ )
113
+
114
+ ## pretty-print the events
115
+ resp.events.each do |e|
116
+ puts("#{set_color(human_time(e.timestamp).utc, :green)} #{set_color(e.log_stream_name, :blue)} #{e.message}")
117
+ end
118
+
119
+ ## token means more data available from this request, so loop and get it right away
120
+ token = resp.next_token
121
+
122
+ ## no token, so sleep and start next request from end time of this one
123
+ unless token
124
+ start_time = end_time
125
+ sleep 10
126
+ end
127
+ end
128
+ end
129
+
130
+ desc 'filter', 'filter events from log group'
131
+ method_option :group, aliases: '-g', type: :string, default: nil, desc: 'log group to filter'
132
+ method_option :streams, aliases: '-s', type: :array, default: nil, desc: 'limit to given streams'
133
+ method_option :pattern, aliases: '-p', type: :string, default: nil, desc: 'pattern to filter logs'
134
+ method_option :start, aliases: '-t', type: :string, default: nil, desc: 'start time'
135
+ method_option :end, aliases: '-e', type: :string, default: nil, desc: 'end time'
136
+ def filter
137
+ trap('SIGINT', 'EXIT') # clean exit with ctrl-c
138
+ group = ((g = options[:group]) ? log_groups[g] : log_groups.values.first).log_group_name
139
+ debug("Log group #{group}")
140
+
141
+ start_time = options[:start] ? Time.parse(options[:start]).to_i*1000 : nil
142
+ end_time = options[:end] ? Time.parse(options[:end]).to_i*1000 : nil
143
+ token = nil
144
+ loop do
145
+ resp = Aws::Logs.client.filter_log_events(
146
+ log_group_name: group,
147
+ log_stream_names: options[:streams],
148
+ next_token: token,
149
+ start_time: start_time,
150
+ end_time: end_time,
151
+ filter_pattern: options[:pattern],
152
+ )
153
+ resp.events.each do |e|
154
+ time = set_color(human_time(e.timestamp).utc, :green)
155
+ stream = set_color(e.log_stream_name, :blue)
156
+ puts("#{time} #{stream} #{e.message}")
157
+ end
158
+ token = resp.next_token
159
+ break unless token
160
+ end
161
+ end
162
+
92
163
  ## lambdas create their own log groups, and when we delete stack they are left behind;
93
164
  ## this task looks up their names by stack prefix, and deletes them
94
165
  desc 'cleanup', 'cleanup lambda log groups named for stack'