pal_tool 0.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +132 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Dockerfile +10 -0
  6. data/Gemfile +14 -0
  7. data/Gemfile.lock +72 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +124 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/exe/pal +47 -0
  14. data/lib/pal/common/local_file_utils.rb +37 -0
  15. data/lib/pal/common/object_helpers.rb +27 -0
  16. data/lib/pal/common/safe_hash_parse.rb +87 -0
  17. data/lib/pal/configuration.rb +77 -0
  18. data/lib/pal/handler/base.rb +138 -0
  19. data/lib/pal/handler/definitions/aws_cur.json +8 -0
  20. data/lib/pal/handler/manager.rb +30 -0
  21. data/lib/pal/handler/processor.rb +84 -0
  22. data/lib/pal/log.rb +29 -0
  23. data/lib/pal/main.rb +63 -0
  24. data/lib/pal/operation/actions.rb +106 -0
  25. data/lib/pal/operation/exporter.rb +183 -0
  26. data/lib/pal/operation/filter_evaluator.rb +249 -0
  27. data/lib/pal/operation/processor_context.rb +50 -0
  28. data/lib/pal/operation/projection.rb +302 -0
  29. data/lib/pal/plugin.rb +61 -0
  30. data/lib/pal/request/metadata.rb +19 -0
  31. data/lib/pal/request/runbook.rb +54 -0
  32. data/lib/pal/version.rb +5 -0
  33. data/lib/pal.rb +43 -0
  34. data/plugins/PLUGINS.md +1 -0
  35. data/plugins/operation/terminal_exporter_impl.rb +14 -0
  36. data/templates/DOCUMENTATION.md +46 -0
  37. data/templates/aws/data_transfer/data_transfer_breakdown.json +93 -0
  38. data/templates/aws/ec2/ec2_compute_hourly_breakdown.json +63 -0
  39. data/templates/aws/ec2/ec2_operation_breakdown.json +64 -0
  40. data/templates/aws/ec2/ec2_spend_breakdown.json +63 -0
  41. data/templates/aws/global_resource_and_usage_type_costs.json +41 -0
  42. data/templates/aws/kms/kms_usage_counts.json +52 -0
  43. data/templates/aws/kms/kms_usage_list.json +80 -0
  44. data/templates/aws/kms/list_of_kms_keys.json +57 -0
  45. data/templates/aws/reserved_instances/all_reserved_instance_expiries.json +41 -0
  46. data/templates/aws/reserved_instances/reserved_instance_opportunities.json +60 -0
  47. data/templates/aws/summary_cost_between_date_range.json +43 -0
  48. data/templates/aws/summary_daily_breakdown_costs.json +39 -0
  49. data/templates/azure/global_resource_type_summary.json +47 -0
  50. metadata +136 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal"
4
+ require "pal/configuration"
5
+ require "pal/common/local_file_utils"
6
+ require "pal/common/safe_hash_parse"
7
+ require "pal/common/object_helpers"
8
+
9
+ module Pal
10
+ module Operation
11
+ # Probably a bad name, its really a processor that calls an export impl - w
12
+ class Exporter
13
+ include Log
14
+ include ObjectHelpers
15
+
16
+ # @return [Array<Pal::Operation::BaseExportHandler>]
17
+ attr_reader :export_types
18
+
19
+ # @return [Array<String>]
20
+ attr_accessor :properties
21
+
22
+ # @return [Actions]
23
+ attr_reader :actions
24
+
25
+ # @param [Pal::Operation::ProcessorContext] ctx
26
+ # @return [Array, Hash]
27
+ def perform_export(ctx)
28
+ log_info("About to extract required data defined in #{ctx.candidates.size} rows")
29
+ extracted_rows, extracted_columns = extract(ctx, @properties)
30
+
31
+ if @actions&.processable?
32
+ log_info("Actions have been defined, going off to extract.")
33
+ extracted_rows, extracted_columns = @actions.process(extracted_rows, extracted_columns)
34
+ end
35
+
36
+ @export_types.each do |t|
37
+ log_info("Exporting for [#{t.class}] triggered ...")
38
+ t.run_export(extracted_rows, extracted_columns)
39
+ log_info("... export for [#{t.class}] completed")
40
+ end
41
+
42
+ [extracted_rows, extracted_columns]
43
+ end
44
+
45
+ private
46
+
47
+ # @param [Array<String>] properties
48
+ # @return [Array]
49
+ # rubocop:disable Metrics/AbcSize
50
+ # rubocop:disable Metrics/MethodLength
51
+ # rubocop:disable Metrics/PerceivedComplexity
52
+ # rubocop:disable Metrics/CyclomaticComplexity
53
+ def extract(ctx, properties)
54
+ all_columns = ctx.column_headers.keys
55
+
56
+ properties = all_columns if properties.nil? || properties.empty?
57
+
58
+ extractable_properties = {}
59
+ properties.each do |property|
60
+ unless all_columns.include? property
61
+ log_warn("[#{property}] not found in column headers.")
62
+ next
63
+ end
64
+
65
+ extractable_properties[property] = ctx.column_headers[property]
66
+ end
67
+
68
+ extracted_rows = ctx.candidates.map do |row|
69
+ extractable_properties.map do |key, value_idx|
70
+ value = row[value_idx]
71
+ value ? ctx.cast(key, value) : "<Missing>"
72
+ end
73
+ end
74
+
75
+ new_extractable_properties = {}
76
+ extractable_properties.keys.each_with_index do |key, idx|
77
+ new_extractable_properties[key] = idx
78
+ end
79
+
80
+ [extracted_rows, new_extractable_properties]
81
+ end
82
+ # rubocop:enable Metrics/AbcSize
83
+ # rubocop:enable Metrics/MethodLength
84
+ # rubocop:enable Metrics/PerceivedComplexity
85
+ # rubocop:enable Metrics/CyclomaticComplexity
86
+
87
+ def actions=(opts)
88
+ @actions = Pal::Operation::Actions.new.from_hash(opts)
89
+ end
90
+
91
+ # @return [Array<Pal::Operation::BaseExportHandler>]
92
+ def types=(types_conf)
93
+ @export_types = types_conf.map do |type_conf|
94
+ name = type_conf["name"]
95
+ settings = type_conf["settings"]
96
+
97
+ clazz_name = "Pal::Operation::#{name.to_s.capitalize}ExporterImpl"
98
+ Kernel.const_get(clazz_name).new(settings)
99
+ end
100
+ end
101
+ end
102
+
103
+ module FileExportable
104
+ # @param [String] file_path
105
+ # @param [String] contents
106
+ # @param [String] file_extension
107
+ def write_to_file(file_path, file_name, file_extension, contents)
108
+ file_location = "#{file_path}/#{file_name || Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S%-z")}"
109
+ Pal::Common::LocalFileUtils.with_file(file_location, file_extension) do |file|
110
+ file.write(contents)
111
+ end
112
+ end
113
+ end
114
+
115
+ class BaseExportHandler
116
+ include Pal::Configuration
117
+ include Pal::Log
118
+
119
+ # @return [Hash] settings
120
+ attr_accessor :settings
121
+
122
+ # @param [Hash] settings
123
+ def initialize(settings)
124
+ @settings = settings
125
+ end
126
+
127
+ # @param [Array] rows
128
+ # @param [Hash] columns
129
+ # Extract values, call export.
130
+ def run_export(rows, columns)
131
+ if rows.empty?
132
+ Pal.logger.warn("No results were found, will not export.")
133
+ return
134
+ end
135
+
136
+ _export(rows, columns)
137
+ end
138
+
139
+ protected
140
+
141
+ # @abstract
142
+ # @param [Array] _rows
143
+ # @param [Hash] _columns
144
+ def _export(_rows, _columns)
145
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
146
+ end
147
+ end
148
+
149
+ class CsvExporterImpl < BaseExportHandler
150
+ include FileExportable
151
+
152
+ # @param [Array] rows
153
+ # @param [Hash] column_headers
154
+ def _export(rows, column_headers)
155
+ file_contents = []
156
+ file_contents << column_headers.keys.join(",")
157
+
158
+ rows.each do |row|
159
+ file_contents << row.join(",")
160
+ end
161
+
162
+ write_to_file(
163
+ @settings["output_dir"], @settings["file_name"] || "pal", "csv", file_contents.join("\n")
164
+ )
165
+ end
166
+ end
167
+
168
+ require "terminal-table"
169
+
170
+ class TableExporterImpl < BaseExportHandler
171
+ # @param [Array] rows
172
+ # @param [Hash] column_headers
173
+ def _export(rows, column_headers)
174
+ title = @settings["title"] || "<No Title Set>"
175
+ style = @settings["style"] || {}
176
+
177
+ table = Terminal::Table.new(title: title, headings: column_headers.keys, rows: rows, style: style)
178
+ puts table
179
+ end
180
+ end
181
+ # do json exporter
182
+ end
183
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "pal/common/object_helpers"
5
+ require "pal/common/safe_hash_parse"
6
+
7
+ module Pal
8
+ module Operation
9
+ # Filter evaluator runs the filter processes to identify candidates
10
+ class FilterEvaluator
11
+ # @return [Rule]
12
+ attr_reader :rule
13
+
14
+ def initialize(filters)
15
+ @rule = RuleFactory.from_hash(filters)
16
+ end
17
+
18
+ # @param [Array] row
19
+ # @param [Hash] column_headers
20
+ # @return [Boolean]
21
+ def test_property(row, column_headers)
22
+ return true if @rule.nil?
23
+
24
+ eval_ctx = EvaluationContext.new(row, column_headers)
25
+ @rule.evaluate(eval_ctx)
26
+ end
27
+ end
28
+
29
+ # Class to manage the rules provided
30
+ class RuleFactory
31
+ # @param [Hash] rule_hash
32
+ # @return [Rule]
33
+ # rubocop:disable Metrics/AbcSize
34
+ def self.from_hash(rule_hash)
35
+ return nil if rule_hash.nil? || rule_hash.keys.empty?
36
+
37
+ if rule_hash.key?("condition")
38
+ condition = rule_hash.fetch("condition")
39
+ rules = RuleFactory.from_group_rules(rule_hash)
40
+
41
+ return AndGroupRule.new(rules) if condition.casecmp("and").zero?
42
+ return OrGroupRule.new(rules) if condition.casecmp("or").zero?
43
+
44
+ raise "Invalid condition [#{condition}] passed."
45
+ end
46
+
47
+ return OperatorRule.new(rule_hash) if rule_hash.key?("operator")
48
+
49
+ raise "Hash is malformed."
50
+ end
51
+ # rubocop:enable Metrics/AbcSize
52
+
53
+ # @return [Array<Rule>]
54
+ def self.from_group_rules(group_rule)
55
+ rules = group_rule.fetch("rules", [])
56
+
57
+ rules.map do |rule_hash|
58
+ from_hash(rule_hash)
59
+ end
60
+ end
61
+ end
62
+
63
+ class Rule
64
+ def evaluate(eval_ctx)
65
+ _evaluate(eval_ctx)
66
+ end
67
+
68
+ private
69
+
70
+ # @abstract
71
+ def _evaluate(_eval_ctx)
72
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
73
+ end
74
+ end
75
+
76
+ class GroupRule < Rule
77
+ # @return [Array<Rule>]
78
+ attr_reader :rule_group
79
+
80
+ # @param [Array<Rule>] rule_group
81
+ def initialize(rule_group)
82
+ @rule_group = rule_group
83
+ super()
84
+ end
85
+ end
86
+
87
+ class AndGroupRule < GroupRule
88
+ private
89
+
90
+ def _evaluate(eval_ctx)
91
+ @rule_group.all? do |rule|
92
+ rule.evaluate(eval_ctx)
93
+ end
94
+ end
95
+ end
96
+
97
+ class OrGroupRule < GroupRule
98
+ private
99
+
100
+ def _evaluate(eval_ctx)
101
+ @rule_group.any? do |rule|
102
+ rule.evaluate(eval_ctx)
103
+ end
104
+ end
105
+ end
106
+
107
+ class OperatorRule < Rule
108
+ # @return [String]
109
+ attr_reader :operator, :field
110
+
111
+ # @return [Object]
112
+ attr_reader :comparison_value
113
+
114
+ # @return [Symbol]
115
+ attr_reader :type
116
+
117
+ def initialize(rule_hash)
118
+ super()
119
+ @field = rule_hash.fetch("field")
120
+ @type = rule_hash.fetch("type").to_sym
121
+ @operator = rule_hash.fetch("operator").to_sym
122
+ @comparison_value = rule_hash.fetch("value")
123
+ end
124
+
125
+ # @param [EvaluationContext] eval_ctx
126
+ # @return [Boolean]
127
+ def _evaluate(eval_ctx)
128
+ operator_candidates = get_operators_for_type(@type)
129
+
130
+ property = eval_ctx.get_value(@field)
131
+ return false unless property
132
+
133
+ proc = operator_candidates.fetch(@operator, proc do |_x, _y|
134
+ raise "Invalid operator given - [#{@operator}]. Valid candidates are [#{operator_candidates.keys.join(", ")}]"
135
+ end)
136
+
137
+ converted_comparison = convert_property(@type, @comparison_value)
138
+ converted_property = convert_property(@type, property)
139
+
140
+ proc.call(converted_property, converted_comparison)
141
+ end
142
+
143
+ # @param [Symbol] type
144
+ # @return [Hash{Symbol->Proc}]
145
+ def get_operators_for_type(type)
146
+ case type
147
+ when :string
148
+ string_operators
149
+ when :number
150
+ number_operators
151
+ when :date
152
+ date_operators
153
+ when :json
154
+ json_operators
155
+ else
156
+ raise "Missing filter operator for [#{type}], valid candidates: [string, number, tag, date]"
157
+ end
158
+ end
159
+
160
+ # @param [Symbol] type
161
+ # @return [Object]
162
+ def convert_property(type, prop)
163
+ case type
164
+ when :string
165
+ prop.to_s
166
+ when :number
167
+ prop.is_a?(Array) ? prop : prop.to_f
168
+ when :json
169
+ prop
170
+ when :date
171
+ Date.parse(prop)
172
+ else
173
+ raise "Missing property operator for [#{type}], valid candidates: [string, number, json, date]"
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ # rubocop:disable Metrics/AbcSize
180
+ # @return [Hash{Symbol->Proc}]
181
+ def string_operators
182
+ {
183
+ equal: proc { |x, y| x == y },
184
+ not_equal: proc { |x, y| x != y },
185
+ begins_with: proc { |x, y| x.start_with?(y) },
186
+ not_begins_with: proc { |x, y| !x.start_with?(y) },
187
+ ends_with: proc { |x, y| x.end_with?(y) },
188
+ not_ends_with: proc { |x, y| !x.end_with?(y) },
189
+ contains: proc { |x, y| x.include?(y) },
190
+ not_contains: proc { |x, y| !x.include?(y) },
191
+ is_empty: proc { |x, _y| x.empty? },
192
+ is_not_empty: proc { |x, _y| !x.empty? }
193
+ }
194
+ end
195
+
196
+ # @return [Hash{Symbol->Proc}]
197
+ def number_operators
198
+ {
199
+ equal: proc { |x, y| x == y },
200
+ not_equal: proc { |x, y| x != y },
201
+ less: proc { |x, y| x < y },
202
+ less_or_equal: proc { |x, y| x <= y },
203
+ greater: proc { |x, y| x > y },
204
+ greater_or_equal: proc { |x, y| x >= y },
205
+ between: proc { |x, y| y.is_a?(Array) ? x.between?(y.first, y.last) : false },
206
+ not_between: proc { |x, y| y.is_a?(Array) ? !x.between?(y.first, y.last) : false }
207
+ }
208
+ end
209
+ alias date_operators number_operators
210
+
211
+ # @return [Hash{Symbol->Proc}]
212
+ def json_operators
213
+ {
214
+ key_equal: proc do |x, y|
215
+ !SafeHashParse.extract_from_json(x, y).empty?
216
+ end,
217
+ key_not_equal: proc do |x, y|
218
+ SafeHashParse.extract_from_json(x, y).empty?
219
+ end,
220
+ value_equal: proc do |x, y|
221
+ SafeHashParse.all_values_from_json(x).include?(y)
222
+ end,
223
+ value_not_equal: proc do |x, y|
224
+ !SafeHashParse.all_values_from_json(x).include?(y)
225
+ end,
226
+ jpath: proc do |x, y|
227
+ path, value = y.split("=")
228
+ SafeHashParse.extract_from_json(x, path).include?(value)
229
+ end
230
+ }
231
+ end
232
+ # rubocop:enable Metrics/AbcSize
233
+ end
234
+
235
+ class EvaluationContext
236
+ attr_accessor :row, :column_headers
237
+
238
+ def initialize(row, column_headers)
239
+ @row = row
240
+ @column_headers = column_headers
241
+ end
242
+
243
+ def get_value(key)
244
+ idx = @column_headers.fetch(key, -1)
245
+ idx.zero? || idx.positive? ? @row[idx] : nil
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Pal
6
+ module Operation
7
+ class ProcessorContext
8
+ attr_accessor :total_row_count, :current_file_row_count, :candidates, :column_headers, :column_type_definitions
9
+
10
+ def initialize
11
+ @total_row_count = 0
12
+ @current_file_row_count = 0
13
+ @candidates = []
14
+ @column_headers = {}
15
+ end
16
+
17
+ # @param [Array<String>] row
18
+ def extract_column_headers(row)
19
+ row.each_with_index { |column, idx| @column_headers[column] = idx }
20
+ end
21
+
22
+ def add_candidate(row)
23
+ @candidates << row
24
+ end
25
+
26
+ # rubocop:disable Metrics/CyclomaticComplexity
27
+ def cast(column_header, value)
28
+ return value unless @column_type_definitions&.key?(column_header)
29
+
30
+ case @column_type_definitions[column_header]["data_type"]
31
+ when "string"
32
+ value.to_s
33
+ when "decimal"
34
+ value.to_f
35
+ when "integer"
36
+ value.to_i
37
+ when "date_time"
38
+ DateTime.parse(value)
39
+ when "date"
40
+ Date.parse(value)
41
+ when nil
42
+ value
43
+ else
44
+ value
45
+ end
46
+ end
47
+ # rubocop:enable Metrics/CyclomaticComplexity
48
+ end
49
+ end
50
+ end