3scale_toolbox 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1faca82c3c88cb4312fa76704e70d656a400f53c3aecb03c507e8a1aeb374284
4
- data.tar.gz: 32c28726d94b60b8238720b1ffe055b06b4ecb1aab3945fd069b899d19f12595
3
+ metadata.gz: bc76523728a2d5fa59805175676eac19b02a689db6c2b6045d821f8b6d5b8fc3
4
+ data.tar.gz: 33908dec9c0714102d0899643e40bcc933b90a412b440325ef8476b4427d8596
5
5
  SHA512:
6
- metadata.gz: 720b320e9e20edecdb93256c2a804ffaff13593f180960e2e10027d5c915c94094a7c2bbf2aca9c9a3b78fc7e9c993bcfc3151d4ac61eab8bf0113a63ed0d5d5
7
- data.tar.gz: c27a4f1aea1076ad163437cdf0b0082763a6f49fa10bcd2045fe63047ea08dc3ba0258833f8fcd8015a8de4604909c9ba7cf9b477b55511999a4c3544dcab5a2
6
+ metadata.gz: ff2630a709bb3af2a8e93cc650359f8d3461444a41438aa261415922a02b9ea9f425dbd027f6d8d46652462544d4e388e9b15ef5066f772bd1ae99457794ead5
7
+ data.tar.gz: 22c140218478ee375e3762e5cb5ffe674e9d1502e7b702d7303341c98d84ad4680ef3887f6608f9ed42cf7bb584a195a099e33d6dfc4196c11201be21b2ca920
data/README.md CHANGED
@@ -2,9 +2,18 @@
2
2
 
3
3
  3scale toolbox is a set of tools to help you manage your 3scale product. Using the [3scale API Ruby Client](https://github.com/3scale/3scale-api-ruby).
4
4
 
5
- ## Installation
6
-
5
+ ## Table of contents
6
+ * [Installation](#installation)
7
+ * [Usage](#usage)
8
+ * [Copy a service](#copy-a-service)
9
+ * [Update a service](#update-a-service)
10
+ * [Import from CSV](#import-from-csv)
11
+ * [Development](#development)
12
+ * [Plugins](#plugins)
13
+ * [Troubleshooting](#troubleshooting)
14
+ * [Contributing](#contributing)
7
15
 
16
+ ## Installation
8
17
  Install the CLI:
9
18
 
10
19
  $ gem install 3scale_toolbox
@@ -12,20 +21,150 @@ Install the CLI:
12
21
  ## Usage
13
22
 
14
23
  ```shell
15
- 3scale help
24
+ $ 3scale help
25
+ NAME
26
+ 3scale - 3scale CLI Toolbox
27
+
28
+ USAGE
29
+ 3scale <command> [options]
30
+
31
+ DESCRIPTION
32
+ 3scale CLI tools to manage your API from the terminal.
33
+
34
+ COMMANDS
35
+ copy 3scale copy command
36
+ help show help
37
+ import 3scale import command
38
+ update 3scale update command
39
+
40
+ OPTIONS
41
+ -k --insecure Proceed and operate even for server connections
42
+ otherwise considered insecure
43
+ -v --version Prints the version of this command
16
44
  ```
17
45
 
18
46
  ### Copy a service
47
+ Will create a new services, copy existing proxy settings, metrics, methods, application plans and mapping rules.
48
+
49
+ Help message:
19
50
 
20
- Will create a new service, copy existing methods, metrics, application plans and their usage limits.
51
+ ```shell
52
+ $ 3scale copy service --help
53
+ NAME
54
+ service - Copy service
55
+
56
+ USAGE
57
+ 3scale copy service [opts] -s <src> -d <dst>
58
+ <service_id>
59
+
60
+ DESCRIPTION
61
+ Will create a new services, copy existing proxy settings, metrics,
62
+ methods, application plans and mapping rules.
63
+
64
+ OPTIONS
65
+ -d --destination=<value> 3scale target instance. Format:
66
+ "http[s]://<provider_key>@3scale_url"
67
+ -s --source=<value> 3scale source instance. Format:
68
+ "http[s]://<provider_key>@3scale_url"
69
+ -t --target_system_name=<value> Target system name
70
+
71
+ OPTIONS FOR COPY
72
+ -h --help show help for this command
73
+ -k --insecure Proceed and operate even for server
74
+ connections otherwise considered
75
+ insecure
76
+ -v --version Prints the version of this command
77
+ ```
21
78
 
22
79
  ```shell
23
80
  3scale copy service NUMBER --source=https://provider_key@foo-admin.3scale.net --destination=https://provider_key@foo2-admin.3scale.net
24
81
  ```
25
82
 
83
+ ### Update a service
84
+
85
+ Will update existing service, update proxy settings, metrics, methods, application plans and mapping rules.
86
+
87
+ Help message:
88
+
89
+ ```shell
90
+ NAME
91
+ service - Update service
92
+
93
+ USAGE
94
+ 3scale update service [opts] -s <src> -d <dst>
95
+ <src_service_id> <dst_service_id>
96
+
97
+ DESCRIPTION
98
+ Will update existing service, update proxy settings, metrics, methods,
99
+ application plans and mapping rules.
100
+
101
+ OPTIONS
102
+ -d --destination=<value> 3scale target instance. Format:
103
+ "http[s]://<provider_key>@3scale_url"
104
+ -f --force Overwrites the mapping rules by deleting
105
+ all rules from target service first
106
+ -r --rules-only Updates only the mapping rules
107
+ -s --source=<value> 3scale source instance. Format:
108
+ "http[s]://<provider_key>@3scale_url"
109
+
110
+ OPTIONS FOR UPDATE
111
+ -h --help show help for this command
112
+ -k --insecure Proceed and operate even for server
113
+ connections otherwise considered insecure
114
+ -v --version Prints the version of this command
115
+ ```
116
+
117
+ Example:
118
+
119
+ ```shell
120
+ $ 3scale update service -s https://9874598743@source.example.com -d https://2342342342342@destination.example.com 3 2
121
+ ```
122
+
26
123
  ### Import from CSV
27
124
 
28
- Will create a new services, metrics, methods and mapping rules.
125
+ Will create new services, metrics, methods, and mapping rules having as source comma separated values (CSV) formatted file.
126
+
127
+ CSV header
128
+
129
+ ```csv
130
+ service_name,endpoint_name,endpoint_http_method,endpoint_path,auth_mode,endpoint_system_name,type
131
+ ```
132
+
133
+ File example
134
+
135
+ ```csv
136
+ service_name,endpoint_name,endpoint_http_method,endpoint_path,auth_mode,endpoint_system_name,type
137
+ Movies ,Movies (Biography),GET,/movies/biography/,api_key,movies_biography,metric
138
+ Movies ,Movies (Drama),GET,/movies/drama/,api_key,movies_drama,method
139
+ ```
140
+
141
+ Help message:
142
+
143
+ ```shell
144
+ $ 3scale import csv -h
145
+ NAME
146
+ csv - Import csv file
147
+
148
+ USAGE
149
+ 3scale import csv [opts] -d <dst> -f <file>
150
+
151
+ DESCRIPTION
152
+ Create new services, metrics, methods and mapping rules from CSV
153
+ formatted file
154
+
155
+ OPTIONS
156
+ -d --destination=<value> 3scale target instance. Format:
157
+ "http[s]://<provider_key>@3scale_url"
158
+ -f --file=<value> CSV formatted file
159
+
160
+ OPTIONS FOR IMPORT
161
+ -h --help show help for this command
162
+ -k --insecure Proceed and operate even for server
163
+ connections otherwise considered insecure
164
+ -v --version Prints the version of this command
165
+ ```
166
+
167
+ Example:
29
168
 
30
169
  ```shell
31
170
  3scale import csv --destination=https://provider_key@user-admin.3scale.net --file=examples/import_example.csv
@@ -37,6 +176,17 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al
37
176
 
38
177
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
39
178
 
179
+ ## Plugins
180
+
181
+ As of 3scale Toolbox 0.5.0, 3scale Toolbox will load plugins installed in gems or $LOAD_PATH. Plugins are discovered via Gem::find_files then loaded.
182
+ Install, uninstall and update plugins using tools like [RubyGems](https://guides.rubygems.org/rubygems-basics/) and/or [Bundler](https://bundler.io/).
183
+
184
+ [Make your own plugin](docs/plugins.md)
185
+
186
+ ## Troubleshooting
187
+
188
+ * [SSL errors](docs/ssl_errors.md): If you run into SSL issues with the toolbox, you can take actions to resolve them.
189
+
40
190
  ## Contributing
41
191
 
42
192
  Bug reports and pull requests are welcome on GitHub at https://github.com/3scale/3scale_toolbox.
data/exe/3scale CHANGED
@@ -1,14 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require '3scale_toolbox'
3
4
  require '3scale_toolbox/cli'
4
5
 
5
- options, argv = ThreeScaleToolbox::CLI.parse
6
+ args = ARGV.clone
6
7
 
7
- unless options.command
8
- puts 'Available subcommands: '
9
- puts ThreeScaleToolbox::CLI.subcommands
10
- puts
11
- ThreeScaleToolbox::CLI.print_help!
12
- end
13
-
14
- exec options.command.full_path, *ARGV
8
+ ThreeScaleToolbox::CLI.run args
@@ -0,0 +1,28 @@
1
+
2
+ module ThreeScaleToolbox
3
+ module Command
4
+ def subcommands
5
+ @subcommands ||= []
6
+ end
7
+
8
+ def add_subcommand(command)
9
+ subcommands << command
10
+ end
11
+
12
+ ##
13
+ # Override to command
14
+ #
15
+ def command
16
+ raise Exception, 'base command has no command definition'
17
+ end
18
+
19
+ ##
20
+ # Iterate recursively over command tree
21
+ #
22
+ def build_command
23
+ subcommands.each_with_object(command) do |subcommand, root_command|
24
+ root_command.add_command(subcommand.build_command)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,78 +1,22 @@
1
- require 'optparse'
1
+ require '3scale_toolbox'
2
+ require '3scale_toolbox/commands'
2
3
 
3
- module ThreeScaleToolbox
4
- module CLI
5
- Options = Struct.new(:command)
6
-
7
- class Parser
8
- def self.parse(options)
9
- args = Options.new(nil)
10
-
11
- opt_parser = OptionParser.new do |opts|
12
- opts.banner = "Usage: 3scale <command> [options]"
13
-
14
-
15
- opts.on("-h", "--help", "Prints this help") do
16
- puts opts
17
- exit
18
- end
19
- end
20
-
21
- begin
22
- opt_parser.order!(options)
23
- rescue OptionParser::InvalidOption => e
24
- p e
25
- end
26
-
27
- return args
28
- end
29
- end
30
-
31
- def self.parse(argv = ARGV)
32
- options = Parser.parse(argv)
33
- options.command = argv.shift
34
- options.command = subcommands.find { |subcommand| subcommand.name == options.command }
35
-
36
- [ options, argv ]
37
- end
38
-
39
- def self.print_help!
40
- Parser.parse %w[--help]
41
- end
42
-
43
- def self.plugins
44
- Gem.loaded_specs.select{ |name, _| name.start_with?('3scale') }.values
45
- end
46
-
47
- def self.current_command
48
- File.expand_path($0, Dir.pwd)
49
- end
50
-
51
- def self.subcommands
52
- plugins
53
- .flat_map { |spec| spec.executables.flat_map{ |bin| Subcommand.new(bin, spec) } }
54
- .reject { |subcommand| subcommand.full_path == current_command || subcommand.name.nil? }
55
- end
56
-
57
- class Subcommand
58
- attr_reader :executable, :spec
59
-
60
- def initialize(executable, spec)
61
- @executable = executable
62
- @spec = spec
63
- end
4
+ module ThreeScaleToolbox::CLI
5
+ def self.root_command
6
+ ThreeScaleToolbox::Commands::ThreeScaleCommand
7
+ end
64
8
 
65
- def to_s
66
- name
67
- end
9
+ def self.add_command(command)
10
+ root_command.add_subcommand(command)
11
+ end
68
12
 
69
- def name
70
- executable.split('-', 2)[1]
71
- end
13
+ def self.load_builtin_commands
14
+ ThreeScaleToolbox::Commands::BUILTIN_COMMANDS.each(&method(:add_command))
15
+ end
72
16
 
73
- def full_path
74
- spec.bin_file(executable)
75
- end
76
- end
17
+ def self.run(args)
18
+ load_builtin_commands
19
+ ThreeScaleToolbox.load_plugins
20
+ root_command.build_command.run args
77
21
  end
78
22
  end
@@ -0,0 +1,28 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/version'
3
+ require '3scale_toolbox/base_command'
4
+
5
+ module ThreeScaleToolbox
6
+ module Commands
7
+ module ThreeScaleCommand
8
+ extend ThreeScaleToolbox::Command
9
+ def self.command
10
+ Cri::Command.define do
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 |_, _|
16
+ puts ThreeScaleToolbox::VERSION
17
+ exit
18
+ end
19
+ flag :k, :insecure, 'Proceed and operate even for server connections otherwise considered insecure'
20
+ flag :h, :help, 'show help for this command' do |_, cmd|
21
+ puts cmd.help
22
+ exit 0
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,218 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/base_command'
3
+
4
+ module ThreeScaleToolbox
5
+ module Commands
6
+ module CopyCommand
7
+ module CopyServiceSubcommand
8
+ extend ThreeScaleToolbox::Command
9
+ def self.command
10
+ Cri::Command.define do
11
+ name 'service'
12
+ 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.'
15
+
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'
19
+
20
+ run do |opts, args, _|
21
+ CopyServiceSubcommand.run opts, args
22
+ end
23
+ end
24
+ end
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*@/, ''
55
+ end
56
+
57
+
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
70
+ end
71
+
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
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end