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.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/Gemfile.lock +1376 -1
- data/lib/lsaws/lister.rb +68 -81
- data/lib/lsaws/sdk_parser.rb +94 -0
- data/lib/lsaws/utils.rb +24 -1
- data/lib/lsaws/version.rb +1 -1
- data/lib/lsaws.rb +8 -0
- data/lsaws.yml +7 -0
- metadata +3 -2
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
|
-
|
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 =
|
18
|
-
if edef.is_a?(String)
|
19
|
-
|
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
|
-
|
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"] ||
|
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
|
-
|
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"] =
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
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
|
181
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
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], *
|
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
|
-
|
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]
|
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.
|
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
|
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
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
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.
|
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-
|
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
|