lsaws 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/lsaws/lister.rb CHANGED
@@ -3,23 +3,22 @@
3
3
  require "aws-sdk-core"
4
4
  require "json"
5
5
  require "tabulo"
6
- require "yaml"
7
6
 
8
7
  module Lsaws
9
8
  class Lister
10
- CONFIG = YAML.load_file(File.join(Lsaws.root, "lsaws.yml"))
9
+ include Utils
10
+
11
+ NEXT_PAGE_FIELDS = %i[next_token next_marker next_page_token page_token].freeze
11
12
 
12
13
  def initialize(options)
13
14
  @options = options
15
+ @options[:max_width] = nil if @options[:max_width].to_i.zero?
14
16
  end
15
17
 
16
- def _prepare_entities(sdk, type)
17
- edef = CONFIG.dig(sdk, type)
18
- if edef.is_a?(String)
19
- type = edef
20
- edef = CONFIG[sdk][type] # should exist
21
- raise Error, "unknown entity type: #{type}" unless edef
22
- end
18
+ def _prepare_entities(sdk, type, &block)
19
+ edef = Lsaws.config.dig(sdk, type)
20
+ return _prepare_entities(sdk, edef, &block) if edef.is_a?(String) # redirect like 'default' -> 'instances'
21
+
23
22
  edef ||= {}
24
23
  require(edef["require"] || "aws-sdk-#{sdk}")
25
24
 
@@ -31,9 +30,10 @@ module Lsaws
31
30
  end
32
31
  params[:max_results] = @options[:max_results] if @options[:max_results]
33
32
 
34
- client_class = edef["client_class"] || guess_client_class(sdk)
33
+ sdkp = SDKParser.new(sdk)
34
+ client_class = edef["client_class"] || sdkp.client_class_name
35
35
  client = Kernel.const_get(client_class).new
36
- method_name = edef["method"] || (client.respond_to?("describe_#{type}") ? "describe_#{type}" : "list_#{type}")
36
+ method_name = edef["method"] || sdkp.etype2method(type)
37
37
  unless client.respond_to?(method_name)
38
38
  if type == "default"
39
39
  warn "[!] no default entity type set for #{sdk.inspect} SDK"
@@ -44,27 +44,41 @@ module Lsaws
44
44
  list_entity_types(sdk)
45
45
  exit 1
46
46
  end
47
+ warn "[d] #{method_name} #{params}" if @options[:debug]
47
48
  results = client.send(method_name, params)
48
49
 
49
- pp results if @options[:debug]
50
+ warn "[d] #{File.basename(__FILE__)}:#{__LINE__} results:\n#{results.pretty_inspect}" if @options[:debug]
50
51
 
51
52
  if !edef["result_keys"] && results.any?
52
53
  r = results.first
53
54
  r = r.last if r.is_a?(Array)
54
- edef["result_keys"] = if r.respond_to?(type)
55
- [type]
56
- else
57
- [(r.members - [:next_token]).first]
58
- end
55
+ edef["result_keys"] =
56
+ if r.respond_to?(type)
57
+ [type]
58
+ elsif NEXT_PAGE_FIELDS.any? { |t| r.respond_to?(t) }
59
+ data_members = r.members - NEXT_PAGE_FIELDS
60
+ if data_members.size == 1
61
+ [data_members[0]]
62
+ else
63
+ # XXX what if there's more than one array?
64
+ [data_members.find { |key| r[key].is_a?(Array) }].compact
65
+ end
66
+ else
67
+ []
68
+ end
59
69
  end
70
+
60
71
  edef["result_keys"].each do |key|
61
72
  results = if results.is_a?(Array)
62
- results.map(&key.to_sym).flatten
73
+ results.map(&key.to_sym).flatten # TODO: is flatten necessary?
63
74
  else
64
- results.send(key)
75
+ results[key]
65
76
  end
66
77
  end
78
+
79
+ warn "[d] #{File.basename(__FILE__)}:#{__LINE__} results:\n#{results.pretty_inspect}" if @options[:debug]
67
80
  edef["cols"] = @options[:show_cols] if @options[:show_cols].any?
81
+ warn "[d] edef: #{edef}" if @options[:debug]
68
82
 
69
83
  col_defs = {}
70
84
  Array(edef["cols"]).each do |r|
@@ -77,10 +91,12 @@ module Lsaws
77
91
  raise Error, "unexpected #{r.inspect}"
78
92
  end
79
93
  end
94
+ # TODO: check with all types
80
95
  col_defs["tags"] = _convert_tags_proc if @options[:show_tags] || col_defs["tags"]
81
96
 
82
97
  results ||= []
83
- if results.any? && !results.first.respond_to?(:name)
98
+ warn "[d] #{results.inspect}" if @options[:debug]
99
+ if results.respond_to?(:any?) && results.any? && !results.first.respond_to?(:name) && results.first.respond_to?(:tags)
84
100
  results.first.class.class_eval do
85
101
  def name
86
102
  tags.find { |tag| tag.key == "Name" }&.value
@@ -88,9 +104,19 @@ module Lsaws
88
104
  end
89
105
  end
90
106
 
91
- # ec2 instance_event_notification_attributes
92
- # 'Array(results)' doesn't work here
93
- results = [results] unless results.is_a?(Array)
107
+ case results
108
+ when Hash
109
+ col_defs = {
110
+ key: :first,
111
+ value: :last
112
+ }
113
+ when Array
114
+ # ok
115
+ else
116
+ # ec2 instance_event_notification_attributes
117
+ # 'Array(results)' doesn't work here
118
+ results = [results]
119
+ end
94
120
 
95
121
  if block_given?
96
122
  results.map do |entity|
@@ -106,6 +132,9 @@ module Lsaws
106
132
  if rows.is_a?(Array) && rows[0].is_a?(String)
107
133
  # sqs
108
134
  cols = { value: proc { |entity| entity } }
135
+ elsif rows.is_a?(Array) && rows[0].is_a?(Hash)
136
+ # securitylake:log_sources
137
+ cols = { value: proc { |entity| entity } }
109
138
  elsif rows.respond_to?(:members)
110
139
  rows = [rows]
111
140
  end
@@ -149,21 +178,8 @@ module Lsaws
149
178
  proc { |entity| entity.tags.map { |tag| "#{tag.key}=#{tag.value}" }.join(", ") }
150
179
  end
151
180
 
152
- def get_sdks
153
- r = []
154
- Gem.path.each do |p|
155
- next unless Dir.exist?(p)
156
-
157
- r.append(*Dir[File.join(p, "gems/aws-sdk-*")].map do |gem_dir|
158
- a = File.basename(gem_dir).split("-")
159
- a.size == 4 ? a[2] : nil
160
- end)
161
- end
162
- r.compact.uniq.sort - ["core"]
163
- end
164
-
165
181
  def list_sdks
166
- _list_array get_sdks
182
+ _list_array SDKParser.get_sdks
167
183
  end
168
184
 
169
185
  def _list_array(a)
@@ -177,55 +193,27 @@ module Lsaws
177
193
  end
178
194
  end
179
195
 
180
- def guess_client_class(sdk)
181
- c = Aws.constants.find { |x| x.to_s.downcase == sdk }
182
- "Aws::#{c}::Client"
183
- end
184
-
185
- def _get_method_rdoc(data, method)
186
- pos = data =~ /^\s+def\s+#{method}\s*\(/
187
- return nil unless pos
188
-
189
- chunk = ""
190
- bs = 4096
191
- until chunk["\n\n"]
192
- chunk = data[pos - bs..pos]
193
- bs *= 2
194
- end
195
- chunk[chunk.rindex("\n\n") + 2..]
196
+ def list_entity_types(sdk)
197
+ _list_array SDKParser.new(sdk).entity_types
196
198
  end
197
199
 
198
- def get_entity_types(sdk)
199
- require "aws-sdk-#{sdk}"
200
- client_class = Kernel.const_get(guess_client_class(sdk))
201
- methods = client_class
202
- .instance_methods
203
- .find_all { |m| m =~ /^(describe|list)_.+s$/ && m !~ /(status|access)$/ }
204
-
205
- return [] if methods.empty?
206
-
207
- data = File.read(client_class.instance_method(methods[0]).source_location[0])
208
- methods.delete_if do |m|
209
- rdoc = _get_method_rdoc(data, m)
210
- next unless rdoc
211
-
212
- required_params = rdoc.scan(/^\s+# @option params \[required, (.+?)\] :(\w+)/)
213
- required_params.any?
200
+ # elasticache:global_replication_groups has 'members' as a column, so cannot just use `rows.members`
201
+ # assuming row is always subclass of Struct here
202
+ def _get_cols(row)
203
+ if row.is_a?(Struct)
204
+ Struct.instance_method(:members).bind_call(row)
205
+ else
206
+ row.members
214
207
  end
215
-
216
- methods.map { |m| m.to_s.sub(/^(describe|list)_/, "") }.sort
217
- end
218
-
219
- def list_entity_types(sdk)
220
- _list_array get_entity_types sdk
221
208
  end
222
209
 
223
210
  def _tabulo_guess_max_cols(rows, _cols)
224
- max_cols = rows[0].members.size
211
+ all_cols = _get_cols(rows[0])
212
+ max_cols = all_cols.size
225
213
  return max_cols if max_cols < 4 || !@options[:max_width]
226
214
 
227
215
  4.upto(max_cols) do |ncols|
228
- tbl = Tabulo::Table.new(rows[0, 100], *rows[0].members[0, ncols])
216
+ tbl = Tabulo::Table.new(rows[0, 100], *all_cols[0, ncols])
229
217
  tbl.autosize_columns
230
218
  tbl_width = tbl.column_registry.values.map { |c| c.padded_width + 1 }.inject(:+) + 1
231
219
  return ncols - 1 if tbl_width >= @options[:max_width]
@@ -239,7 +227,7 @@ module Lsaws
239
227
  elsif type == :list
240
228
  return list_entity_types(sdk)
241
229
  elsif type == :all
242
- get_entity_types(sdk).each do |etype|
230
+ SDKParser.new(sdk).entity_types.each do |etype|
243
231
  puts "#{etype}:"
244
232
  process_command(sdk, etype)
245
233
  end
@@ -260,7 +248,7 @@ module Lsaws
260
248
  cols.each { |name, func| tbl.add_column(name, &func) }
261
249
  else
262
250
  max_cols = _tabulo_guess_max_cols(rows, cols)
263
- rows[0].members[0, max_cols].each { |col| tbl.add_column(col) }
251
+ _get_cols(rows[0])[0, max_cols].each { |col| tbl.add_column(col) }
264
252
  end
265
253
  puts tbl.pack(max_table_width: @options[:max_width])
266
254
  when :json
@@ -273,8 +261,7 @@ module Lsaws
273
261
  end
274
262
  when :yaml
275
263
  rows = entities2hashes(sdk, type)
276
- rows.each { |row| Utils._deep_transform_keys_in_object!(row, &:to_s) }
277
- puts rows.to_yaml
264
+ puts rows.map { |row| _deep_transform_keys_in_object(row, &:to_s) }.to_yaml
278
265
  else
279
266
  warn "[!] unknown format: #{@options[:format]}"
280
267
  exit 1
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lsaws
4
+ class SDKParser
5
+ IGNORED_SDKS = [
6
+ "core", "resources", # do not contain any resource listing methods
7
+ "s3control" # requires account_id param for all requests
8
+ ].freeze
9
+
10
+ def self.get_sdks
11
+ r = []
12
+ Gem.path.each do |p|
13
+ next unless Dir.exist?(p)
14
+
15
+ r.append(*Dir[File.join(p, "gems/aws-sdk-*")].map do |gem_dir|
16
+ a = File.basename(gem_dir).split("-")
17
+ a.size == 4 ? a[2] : nil
18
+ end)
19
+ end
20
+ r.compact.uniq.sort - IGNORED_SDKS
21
+ end
22
+
23
+ def initialize(sdk)
24
+ @sdk = sdk
25
+ require "aws-sdk-#{sdk}"
26
+ end
27
+
28
+ def client_class_name
29
+ @client_class_name ||=
30
+ begin
31
+ # TODO: use constants from gems/aws-sdk-resources-x.xxx
32
+ c = Aws.constants.find { |x| x.to_s.downcase == @sdk }
33
+ "Aws::#{c}::Client"
34
+ end
35
+ end
36
+
37
+ def client_class
38
+ @client_class ||= Kernel.const_get(client_class_name)
39
+ end
40
+
41
+ # order is important!
42
+ LIST_METHOD_PREFIXES = %w[list describe get].freeze
43
+
44
+ def etype2method(etype)
45
+ LIST_METHOD_PREFIXES.each do |prefix|
46
+ m = "#{prefix}_#{etype}"
47
+ return m if client_class.public_method_defined?(m)
48
+ end
49
+ nil
50
+ end
51
+
52
+ def method2etype(method)
53
+ method.to_s.sub(/^(?:#{LIST_METHOD_PREFIXES.join("|")})_/, "")
54
+ end
55
+
56
+ def entity_types
57
+ methods = client_class
58
+ .instance_methods
59
+ .find_all { |m| m =~ /^(?:#{LIST_METHOD_PREFIXES.join("|")})_.+s$/ && m !~ /(?:status|access)$/ }
60
+
61
+ return [] if methods.empty?
62
+
63
+ methods.delete_if do |m|
64
+ rdoc = get_method_rdoc(m)
65
+ next(true) unless rdoc
66
+
67
+ required_params = rdoc.scan(/^\s+# @option params \[required, (.+?)\] :(\w+)/)
68
+ required_params.any? || Lsaws.config.dig(@sdk, method2etype(m), "required_params")
69
+ end
70
+
71
+ methods.map { |m| method2etype(m) }.sort
72
+ end
73
+
74
+ def get_method_rdoc(method)
75
+ @source ||= File.read(client_class.instance_method(method).source_location[0])
76
+
77
+ pos = @source =~ /^\s+def\s+#{method}\s*\(/
78
+ return nil unless pos
79
+
80
+ chunk = ""
81
+ bs = 4096
82
+ until chunk["\n\n"]
83
+ chunk = @source[pos - bs..pos]
84
+ bs *= 2
85
+ end
86
+ chunk[chunk.rindex("\n\n") + 2..]
87
+ end
88
+
89
+ def get_method_api(method)
90
+ # calling private API!
91
+ client_class.api.operation(method)
92
+ end
93
+ end
94
+ end
data/lib/lsaws/utils.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Lsaws
4
4
  module Utils
5
5
  # copypasted from lib/active_support/core_ext/hash/keys.rb
6
- def self._deep_transform_keys_in_object!(object, &block)
6
+ def _deep_transform_keys_in_object!(object, &block)
7
7
  case object
8
8
  when Hash
9
9
  object.each_key do |key|
@@ -17,5 +17,28 @@ module Lsaws
17
17
  object
18
18
  end
19
19
  end
20
+
21
+ def _deep_transform_keys_in_object(object, &block)
22
+ case object
23
+ when Hash
24
+ object.each_with_object(object.class.new) do |(key, value), result|
25
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
26
+ end
27
+ when Array
28
+ object.map { |e| _deep_transform_keys_in_object(e, &block) }
29
+ else
30
+ object
31
+ end
32
+ end
33
+
34
+ # "2022-11-15 10:49:00 UTC" => "2022-11-15T10:49:00+00:00"
35
+ def to_iso8601(s)
36
+ s.gsub(/([0-9-]{10}) ([0-9:]+{8}) UTC/, '\1T\2+00:00')
37
+ end
38
+
39
+ # "2022-11-15T10:49:00+00:00" => "2022-11-15 10:49:00 UTC"
40
+ def from_iso8601(s)
41
+ s.gsub(/([0-9-]{10})T([0-9:]+{8})\+00:00/, '\1 \2 UTC')
42
+ end
20
43
  end
21
44
  end
data/lib/lsaws/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lsaws
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/lsaws.rb CHANGED
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+
3
5
  module Lsaws
4
6
  class Error < StandardError; end
5
7
 
6
8
  def self.root
7
9
  File.dirname(File.dirname(File.expand_path(__FILE__)))
8
10
  end
11
+
12
+ def self.config
13
+ @config ||= YAML.load_file(File.join(Lsaws.root, "lsaws.yml"))
14
+ end
9
15
  end
10
16
 
11
17
  require_relative "lsaws/version"
18
+ require_relative "lsaws/sdk_parser"
12
19
  require_relative "lsaws/cli"
20
+ require_relative "lsaws/utils"
13
21
  require_relative "lsaws/lister"
data/lsaws.yml CHANGED
@@ -1,3 +1,8 @@
1
+ cloudformation:
2
+ stack_resources:
3
+ required_params:
4
+ - [ stack_name ]
5
+ - [ physical_resource_id ]
1
6
  ec2:
2
7
  default: instances
3
8
  images:
@@ -39,3 +44,5 @@ kms:
39
44
  - alias_name
40
45
  - target_key_id
41
46
  - last_updated_date
47
+ s3:
48
+ default: buckets
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lsaws
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey "Zed" Zaikin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-09 00:00:00.000000000 Z
11
+ date: 2023-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-core
@@ -59,6 +59,7 @@ files:
59
59
  - lib/lsaws.rb
60
60
  - lib/lsaws/cli.rb
61
61
  - lib/lsaws/lister.rb
62
+ - lib/lsaws/sdk_parser.rb
62
63
  - lib/lsaws/utils.rb
63
64
  - lib/lsaws/version.rb
64
65
  - lsaws.gemspec