lsaws 0.1.0 → 0.3.0

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.
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