3scale_toolbox 0.5.1 → 0.6.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +143 -23
  3. data/exe/3scale +10 -3
  4. data/lib/3scale_toolbox.rb +18 -0
  5. data/lib/3scale_toolbox/3scale_client_factory.rb +33 -0
  6. data/lib/3scale_toolbox/base_command.rb +52 -14
  7. data/lib/3scale_toolbox/cli.rb +26 -5
  8. data/lib/3scale_toolbox/cli/error_handler.rb +120 -0
  9. data/lib/3scale_toolbox/commands.rb +3 -9
  10. data/lib/3scale_toolbox/commands/3scale_command.rb +8 -6
  11. data/lib/3scale_toolbox/commands/copy_command.rb +4 -4
  12. data/lib/3scale_toolbox/commands/copy_command/copy_service.rb +40 -193
  13. data/lib/3scale_toolbox/commands/help_command.rb +1 -1
  14. data/lib/3scale_toolbox/commands/import_command.rb +6 -4
  15. data/lib/3scale_toolbox/commands/import_command/import_csv.rb +15 -41
  16. data/lib/3scale_toolbox/commands/import_command/openapi.rb +70 -0
  17. data/lib/3scale_toolbox/commands/import_command/openapi/create_mapping_rule_step.rb +18 -0
  18. data/lib/3scale_toolbox/commands/import_command/openapi/create_method_step.rb +39 -0
  19. data/lib/3scale_toolbox/commands/import_command/openapi/create_service_step.rb +69 -0
  20. data/lib/3scale_toolbox/commands/import_command/openapi/mapping_rule.rb +35 -0
  21. data/lib/3scale_toolbox/commands/import_command/openapi/method.rb +25 -0
  22. data/lib/3scale_toolbox/commands/import_command/openapi/operation.rb +22 -0
  23. data/lib/3scale_toolbox/commands/import_command/openapi/resource_reader.rb +49 -0
  24. data/lib/3scale_toolbox/commands/import_command/openapi/step.rb +45 -0
  25. data/lib/3scale_toolbox/commands/import_command/openapi/threescale_api_spec.rb +33 -0
  26. data/lib/3scale_toolbox/commands/remote_command.rb +36 -0
  27. data/lib/3scale_toolbox/commands/remote_command/remote_add.rb +47 -0
  28. data/lib/3scale_toolbox/commands/remote_command/remote_list.rb +29 -0
  29. data/lib/3scale_toolbox/commands/remote_command/remote_remove.rb +26 -0
  30. data/lib/3scale_toolbox/commands/remote_command/remote_rename.rb +42 -0
  31. data/lib/3scale_toolbox/commands/update_command.rb +4 -4
  32. data/lib/3scale_toolbox/commands/update_command/update_service.rb +45 -235
  33. data/lib/3scale_toolbox/configuration.rb +35 -0
  34. data/lib/3scale_toolbox/entities.rb +1 -0
  35. data/lib/3scale_toolbox/entities/service.rb +113 -0
  36. data/lib/3scale_toolbox/error.rb +8 -0
  37. data/lib/3scale_toolbox/helper.rb +37 -0
  38. data/lib/3scale_toolbox/remotes.rb +93 -0
  39. data/lib/3scale_toolbox/tasks.rb +10 -0
  40. data/lib/3scale_toolbox/tasks/copy_app_plans_task.rb +31 -0
  41. data/lib/3scale_toolbox/tasks/copy_limits_task.rb +36 -0
  42. data/lib/3scale_toolbox/tasks/copy_mapping_rules_task.rb +29 -0
  43. data/lib/3scale_toolbox/tasks/copy_methods_task.rb +29 -0
  44. data/lib/3scale_toolbox/tasks/copy_metrics_task.rb +33 -0
  45. data/lib/3scale_toolbox/tasks/copy_service_proxy_task.rb +12 -0
  46. data/lib/3scale_toolbox/tasks/copy_task.rb +26 -0
  47. data/lib/3scale_toolbox/tasks/destroy_mapping_rules_task.rb +22 -0
  48. data/lib/3scale_toolbox/tasks/helper_task.rb +25 -0
  49. data/lib/3scale_toolbox/tasks/update_service_settings_task.rb +32 -0
  50. data/lib/3scale_toolbox/version.rb +1 -1
  51. metadata +87 -11
@@ -0,0 +1,120 @@
1
+ require 'json'
2
+
3
+ module ThreeScaleToolbox
4
+ module CLI
5
+ class ErrorHandler
6
+ def self.error_watchdog
7
+ new.error_watchdog { yield }
8
+ end
9
+
10
+ # Catches errors and prints nice diagnostic messages
11
+ def error_watchdog
12
+ # Run
13
+ yield
14
+ rescue StandardError, ScriptError => e
15
+ handle_error e
16
+ e
17
+ else
18
+ nil
19
+ end
20
+
21
+ private
22
+
23
+ def handle_error(error)
24
+ if expected_error?(error)
25
+ warn
26
+ warn "\e[1m\e[31mError: #{error.message}\e[0m"
27
+ else
28
+ print_error(error)
29
+ end
30
+ end
31
+
32
+ def expected_error?(error)
33
+ case error
34
+ when ThreeScaleToolbox::Error
35
+ true
36
+ else
37
+ false
38
+ end
39
+ end
40
+
41
+ def print_error(error)
42
+ write_error(error, $stderr)
43
+
44
+ File.open('crash.log', 'w') do |io|
45
+ write_verbose_error(error, io)
46
+ end
47
+
48
+ write_section_header($stderr, 'Detailed information')
49
+ warn
50
+ warn 'A detailed crash log has been written to ./crash.log.'
51
+ end
52
+
53
+ def write_error(error, stream)
54
+ write_error_message(error, stream)
55
+ write_stack_trace(error, stream)
56
+ end
57
+
58
+ def write_error_message(error, stream)
59
+ write_section_header(stream, 'Message')
60
+ stream.puts "\e[1m\e[31m#{error.class}: #{error.message}\e[0m"
61
+ end
62
+
63
+ def write_stack_trace(error, stream)
64
+ write_section_header(stream, 'Backtrace')
65
+ stream.puts error.backtrace
66
+ end
67
+
68
+ def write_version_information(stream)
69
+ write_section_header(stream, 'Version Information')
70
+ stream.puts ThreeScaleToolbox::VERSION
71
+ end
72
+
73
+ def write_system_information(stream)
74
+ write_section_header(stream, 'System Information')
75
+ stream.puts Etc.uname.to_json
76
+ end
77
+
78
+ def write_installed_gems(stream)
79
+ write_section_header(stream, 'Installed gems')
80
+ gems_and_versions.each do |g|
81
+ stream.puts " #{g.first} #{g.last.join(', ')}"
82
+ end
83
+ end
84
+
85
+ def write_load_paths(stream)
86
+ write_section_header(stream, 'Load paths')
87
+ $LOAD_PATH.each_with_index do |i, index|
88
+ stream.puts " #{index}. #{i}"
89
+ end
90
+ end
91
+
92
+ def write_verbose_error(error, stream)
93
+ stream.puts "Crashlog created at #{Time.now}"
94
+
95
+ write_error_message(error, stream)
96
+ write_stack_trace(error, stream)
97
+ write_version_information(stream)
98
+ write_system_information(stream)
99
+ write_installed_gems(stream)
100
+ write_load_paths(stream)
101
+ end
102
+
103
+ def gems_and_versions
104
+ gems = {}
105
+ Gem::Specification.find_all.sort_by { |s| [s.name, s.version] }.each do |spec|
106
+ gems[spec.name] ||= []
107
+ gems[spec.name] << spec.version.to_s
108
+ end
109
+ gems
110
+ end
111
+
112
+ def write_section_header(stream, title)
113
+ stream.puts
114
+
115
+ stream.puts "===== #{title.upcase}:"
116
+ stream.puts
117
+ end
118
+ end
119
+ end
120
+ end
@@ -3,6 +3,7 @@ require '3scale_toolbox/commands/help_command'
3
3
  require '3scale_toolbox/commands/copy_command'
4
4
  require '3scale_toolbox/commands/import_command'
5
5
  require '3scale_toolbox/commands/update_command'
6
+ require '3scale_toolbox/commands/remote_command'
6
7
 
7
8
  module ThreeScaleToolbox
8
9
  module Commands
@@ -10,15 +11,8 @@ module ThreeScaleToolbox
10
11
  ThreeScaleToolbox::Commands::HelpCommand,
11
12
  ThreeScaleToolbox::Commands::CopyCommand,
12
13
  ThreeScaleToolbox::Commands::ImportCommand,
13
- ThreeScaleToolbox::Commands::UpdateCommand
14
+ ThreeScaleToolbox::Commands::UpdateCommand,
15
+ ThreeScaleToolbox::Commands::RemoteCommand::RemoteCommand
14
16
  ].freeze
15
-
16
- def self.service_valid_params
17
- %w[
18
- name backend_version deployment_option description
19
- system_name end_user_registration_required
20
- support_email tech_support_email admin_support_email
21
- ]
22
- end
23
17
  end
24
18
  end
@@ -5,16 +5,18 @@ require '3scale_toolbox/base_command'
5
5
  module ThreeScaleToolbox
6
6
  module Commands
7
7
  module ThreeScaleCommand
8
- extend ThreeScaleToolbox::Command
8
+ include ThreeScaleToolbox::Command
9
9
  def self.command
10
10
  Cri::Command.define do
11
11
  name '3scale'
12
- usage '3scale <command> [options]'
13
- summary '3scale CLI Toolbox'
14
- description '3scale CLI tools to manage your API from the terminal.'
15
- flag :v, :version, 'Prints the version of this command' do |_, _|
12
+ usage '3scale <sub-command> [options]'
13
+ summary '3scale toolbox'
14
+ description '3scale toolbox to manage your API from the terminal.'
15
+ option :c, 'config-file', '3scale toolbox configuration file',
16
+ argument: :required, default: ThreeScaleToolbox.default_config_file
17
+ flag :v, :version, 'Prints the version of this command' do
16
18
  puts ThreeScaleToolbox::VERSION
17
- exit
19
+ exit 0
18
20
  end
19
21
  flag :k, :insecure, 'Proceed and operate even for server connections otherwise considered insecure'
20
22
  flag :h, :help, 'show help for this command' do |_, cmd|
@@ -5,13 +5,13 @@ require '3scale_toolbox/commands/copy_command/copy_service'
5
5
  module ThreeScaleToolbox
6
6
  module Commands
7
7
  module CopyCommand
8
- extend ThreeScaleToolbox::Command
8
+ include ThreeScaleToolbox::Command
9
9
  def self.command
10
10
  Cri::Command.define do
11
11
  name 'copy'
12
- usage 'copy <command> [options]'
13
- summary '3scale copy command'
14
- description '3scale copy command.'
12
+ usage 'copy <sub-command> [options]'
13
+ summary 'copy super command'
14
+ description 'Copy 3scale entities between tenants'
15
15
  end
16
16
  end
17
17
  add_subcommand(CopyServiceSubcommand)
@@ -4,213 +4,60 @@ require '3scale_toolbox/base_command'
4
4
  module ThreeScaleToolbox
5
5
  module Commands
6
6
  module CopyCommand
7
- module CopyServiceSubcommand
8
- extend ThreeScaleToolbox::Command
7
+ class CopyServiceSubcommand < Cri::CommandRunner
8
+ include ThreeScaleToolbox::Command
9
+
9
10
  def self.command
10
11
  Cri::Command.define do
11
12
  name 'service'
12
13
  usage 'service [opts] -s <src> -d <dst> <service_id>'
13
- summary 'Copy service'
14
- description 'Will create a new services, copy existing proxy settings, metrics, methods, application plans and mapping rules.'
14
+ summary 'copy service'
15
+ description 'will create a new services, copy existing proxy settings, metrics, methods, application plans and mapping rules.'
15
16
 
16
- required :s, :source, '3scale source instance. Format: "http[s]://<provider_key>@3scale_url"'
17
- required :d, :destination, '3scale target instance. Format: "http[s]://<provider_key>@3scale_url"'
18
- required :t, 'target_system_name', 'Target system name'
17
+ option :s, :source, '3scale source instance. Url or remote name', argument: :required
18
+ option :d, :destination, '3scale target instance. Url or remote name', argument: :required
19
+ option :t, 'target_system_name', 'Target system name', argument: :required
20
+ param :service_id
19
21
 
20
- run do |opts, args, _|
21
- CopyServiceSubcommand.run opts, args
22
- end
22
+ runner CopyServiceSubcommand
23
23
  end
24
24
  end
25
25
 
26
- def self.run(opts, args)
27
- source = fetch_required_option(opts, :source)
28
- destination = fetch_required_option(opts, :destination)
29
- system_name = fetch_required_option(opts, :target_system_name)
30
- insecure = opts[:insecure] || false
31
- exit_with_message 'error: missing service_id argument' if args.empty?
32
- service_id = args[0]
33
- copy_service(service_id, source, destination, system_name, insecure)
34
- end
35
-
36
- def self.exit_with_message(message)
37
- puts message
38
- exit 1
39
- end
40
-
41
- def self.fetch_required_option(options, key)
42
- options.fetch(key) { exit_with_message "error: Missing argument #{key}" }
43
- end
44
-
45
- def self.compare_hashes(first, second, keys)
46
- keys.map{ |key| first.fetch(key) } == keys.map{ |key| second.fetch(key) }
47
- end
48
-
49
- def self.provider_key_from_url(url)
50
- url[/\w*@/][0..-2]
51
- end
52
-
53
- def self.endpoint_from_url(url)
54
- url.sub /\w*@/, ''
26
+ def run
27
+ source = fetch_required_option(:source)
28
+ destination = fetch_required_option(:destination)
29
+ system_name = fetch_required_option(:target_system_name)
30
+
31
+ source_service = Entities::Service.new(id: arguments[:service_id],
32
+ remote: threescale_client(source))
33
+ target_service = create_new_service(source_service.show_service, destination, system_name)
34
+ puts "new service id #{target_service.id}"
35
+ context = create_context(source_service, target_service)
36
+ tasks = [
37
+ Tasks::CopyServiceProxyTask.new(context),
38
+ Tasks::CopyMethodsTask.new(context),
39
+ Tasks::CopyMetricsTask.new(context),
40
+ Tasks::CopyApplicationPlansTask.new(context),
41
+ Tasks::CopyLimitsTask.new(context),
42
+ Tasks::DestroyMappingRulesTask.new(context),
43
+ Tasks::CopyMappingRulesTask.new(context)
44
+ ]
45
+ tasks.each(&:call)
55
46
  end
56
47
 
48
+ private
57
49
 
58
- # Returns new hash object with not nil valid params
59
- def self.filter_params(valid_params, source)
60
- valid_params.each_with_object({}) do |key, target|
61
- target[key] = source[key] unless source[key].nil?
62
- end
63
- end
64
-
65
- def self.copy_service_params(original, system_name)
66
- service_params = filter_params(Commands.service_valid_params, original)
67
- service_params.tap do |hash|
68
- hash['system_name'] = system_name if system_name
69
- end
50
+ def create_context(source, target)
51
+ {
52
+ source: source,
53
+ target: target
54
+ }
70
55
  end
71
56
 
72
- def self.copy_service(service_id, source, destination, system_name, insecure)
73
- require '3scale/api'
74
-
75
- source_client = ThreeScale::API.new(
76
- endpoint: endpoint_from_url(source),
77
- provider_key: provider_key_from_url(source),
78
- verify_ssl: !insecure
79
- )
80
- client = ThreeScale::API.new(
81
- endpoint: endpoint_from_url(destination),
82
- provider_key: provider_key_from_url(destination),
83
- verify_ssl: !insecure
84
- )
85
-
86
- service = source_client.show_service(service_id)
87
- copy = client.create_service(copy_service_params(service, system_name))
88
-
89
- raise "Service has not been saved. Errors: #{copy['errors']}" unless copy['errors'].nil?
90
-
91
- service_copy_id = copy.fetch('id')
92
-
93
- puts "new service id #{service_copy_id}"
94
-
95
- proxy = source_client.show_proxy(service_id)
96
- client.update_proxy(service_copy_id, proxy)
97
- puts "updated proxy of #{service_copy_id} to match the original"
98
-
99
- metrics = source_client.list_metrics(service_id)
100
- metrics_copies = client.list_metrics(service_copy_id)
101
-
102
- hits = metrics.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
103
- hits_copy = metrics_copies.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
104
-
105
- methods = source_client.list_methods(service_id, hits['id'])
106
- methods_copies = client.list_methods(service_copy_id, hits_copy['id'])
107
-
108
- puts "original service hits metric #{hits['id']} has #{methods.size} methods"
109
- puts "copied service hits metric #{hits_copy['id']} has #{methods_copies.size} methods"
110
-
111
- missing_methods = methods.reject { |method| methods_copies.find{|copy| compare_hashes(method, copy, ['system_name']) } }
112
-
113
- puts "creating #{missing_methods.size} missing methods on copied service"
114
-
115
- missing_methods.each do |method|
116
- copy = { friendly_name: method['friendly_name'], system_name: method['system_name'] }
117
- client.create_method(service_copy_id, hits_copy['id'], copy)
118
- end
119
-
120
- metrics_copies = client.list_metrics(service_copy_id)
121
-
122
- puts "original service has #{metrics.size} metrics"
123
- puts "copied service has #{metrics_copies.size} metrics"
124
-
125
- missing_metrics = metrics.reject { |metric| metrics_copies.find{|copy| compare_hashes(metric, copy, ['system_name']) } }
126
-
127
- missing_metrics.map do |metric|
128
- metric.delete('links')
129
- client.create_metric(service_copy_id, metric)
130
- end
131
-
132
- puts "created #{missing_metrics.size} metrics on the copied service"
133
-
134
- plans = source_client.list_service_application_plans(service_id)
135
- plan_copies = client.list_service_application_plans(service_copy_id)
136
-
137
- puts "original service has #{plans.size} application plans "
138
- puts "copied service has #{plan_copies.size} application plans"
139
-
140
- missing_application_plans = plans.reject { |plan| plan_copies.find{|copy| plan.fetch('system_name') == copy.fetch('system_name') } }
141
-
142
- puts "copied service missing #{missing_application_plans.size} application plans"
143
-
144
- missing_application_plans.each do |plan|
145
- plan.delete('links')
146
- plan.delete('default') # TODO: handle default plan
147
-
148
- if plan.delete('custom') # TODO: what to do with custom plans?
149
- puts "skipping custom plan #{plan}"
150
- else
151
- client.create_application_plan(service_copy_id, plan)
152
- end
153
- end
154
-
155
- application_plan_mapping = client.list_service_application_plans(service_copy_id).map do |plan_copy|
156
- plan = plans.find{|plan| plan.fetch('system_name') == plan_copy.fetch('system_name') }
157
-
158
- [plan['id'], plan_copy['id']]
159
- end
160
-
161
- metrics_mapping = client.list_metrics(service_copy_id).map do |copy|
162
- metric = metrics.find{|metric| metric.fetch('system_name') == copy.fetch('system_name') }
163
- metric ||= {}
164
-
165
- [metric['id'], copy['id']]
166
- end.to_h
167
-
168
- puts "destroying all mapping rules of the copy which have been created by default"
169
- client.list_mapping_rules(service_copy_id).each do |mapping_rule|
170
- client.delete_mapping_rule(service_copy_id, mapping_rule['id'])
171
- end
172
-
173
- mapping_rules = source_client.list_mapping_rules(service_id)
174
- mapping_rules_copy = client.list_mapping_rules(service_copy_id)
175
-
176
- puts "the original service has #{mapping_rules.size} mapping rules"
177
- puts "the copy has #{mapping_rules_copy.size} mapping rules"
178
-
179
- unique_mapping_rules_copy = mapping_rules_copy.dup
180
-
181
- missing_mapping_rules = mapping_rules.reject do |mapping_rule|
182
- matching_metric = unique_mapping_rules_copy.find do |copy|
183
- compare_hashes(mapping_rule, copy, %w(pattern http_method delta)) &&
184
- metrics_mapping.fetch(mapping_rule.fetch('metric_id')) == copy.fetch('metric_id')
185
- end
186
-
187
- unique_mapping_rules_copy.delete(matching_metric)
188
- end
189
-
190
- puts "missing #{missing_mapping_rules.size} mapping rules"
191
-
192
- missing_mapping_rules.each do |mapping_rule|
193
- mapping_rule.delete('links')
194
- mapping_rule['metric_id'] = metrics_mapping.fetch(mapping_rule.delete('metric_id'))
195
- client.create_mapping_rule(service_copy_id, mapping_rule)
196
- end
197
- puts "created #{missing_mapping_rules.size} mapping rules"
198
-
199
- puts "extra #{unique_mapping_rules_copy.size} mapping rules"
200
- puts unique_mapping_rules_copy.each{|rule| rule.delete('links') }
201
-
202
- application_plan_mapping.each do |original_id, copy_id|
203
- limits = source_client.list_application_plan_limits(original_id)
204
- limits_copy = client.list_application_plan_limits(copy_id)
205
-
206
- missing_limits = limits.reject { |limit| limits_copy.find{|limit_copy| limit.fetch('period') == limit_copy.fetch('period') } }
207
-
208
- missing_limits.each do |limit|
209
- limit.delete('links')
210
- client.create_application_plan_limit(copy_id, metrics_mapping.fetch(limit.fetch('metric_id')), limit)
211
- end
212
- puts "copied application plan #{copy_id} is missing #{missing_limits.size} from the original plan #{original_id}"
213
- end
57
+ def create_new_service(service, destination, system_name)
58
+ Entities::Service.create(remote: threescale_client(destination),
59
+ service: service,
60
+ system_name: system_name)
214
61
  end
215
62
  end
216
63
  end
@@ -4,7 +4,7 @@ require '3scale_toolbox/base_command'
4
4
  module ThreeScaleToolbox
5
5
  module Commands
6
6
  module HelpCommand
7
- extend ThreeScaleToolbox::Command
7
+ include ThreeScaleToolbox::Command
8
8
  def self.command
9
9
  Cri::Command.new_basic_help
10
10
  end