Skip to content

Commit

Permalink
Merge pull request #76 from 3scale/openapi2-import
Browse files Browse the repository at this point in the history
Import OpenAPI v2.0 command
  • Loading branch information
eguzki authored Jan 21, 2019
2 parents b3b52b1 + 1cacde2 commit d02f2cb
Show file tree
Hide file tree
Showing 39 changed files with 2,030 additions and 78 deletions.
1 change: 1 addition & 0 deletions 3scale_toolbox.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ Gem::Specification.new do |spec|

spec.add_dependency '3scale-api', '~> 0.1.7'
spec.add_dependency 'cri', '~> 2.15'
spec.add_dependency 'swagger-core', '~> 0.3'
end
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* [Copy a service](#copy-a-service)
* [Update a service](#update-a-service)
* [Import from CSV](#import-from-csv)
* [Import from OpenAPI definition](#import-openapi)
* [Remotes](#remotes)
* [Development](#development)
* [Testing](#testing)
Expand Down Expand Up @@ -182,6 +183,14 @@ Example:
3scale import csv --destination=https://[email protected] --file=examples/import_example.csv
```

### Import OpenAPI

Using an API definition format like OpenAPI, import to your 3scale API

Currently, only OpenAPI __2.0__ specification (f.k.a. __swagger__) is supported.

[Import from OpenAPI](docs/openapi.md)

### Remotes

Manage set of 3scale instances.
Expand Down
82 changes: 82 additions & 0 deletions docs/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
## Import API definition to 3scale from OpenAPI definition

Features:

* OpenAPI __2.0__ specification (f.k.a. __swagger__)
* Create a new service. New service name will be taken from openapi definition `info.title` field.
* Update existing service, providing `SERVICE_ID` with `--service` option.
* Create methods in the 'Definition' section. Method names are taken from `operation.operationId` field.
* Attach newly created methods to the *Hits* metric.
* Create mapping rules and show them under `API > Integration`.
* Perform schema validation.
* OpenAPI definition resource can be provided by one of the following channels:
* *Filename* in the available path.
* *URL* format. Toolbox will try to download from given address.
* Read from *stdin* standard input stream.

### Usage

```shell
$ 3scale import openapi -h
NAME
openapi - Import API defintion in OpenAPI specification

USAGE
3scale import openapi [opts] -d <dst> <spec>

DESCRIPTION
Using an API definition format like OpenAPI, import to your 3scale API

OPTIONS
-d --destination=<value> 3scale target instance. Url or
remote name
-s --service=<value> <service_id> of your 3scale account

OPTIONS FOR IMPORT
-c --config-file=<value> 3scale CLI configuration file (default:
/home/eguzki/.3scalerc.yaml)
-h --help show help for this command
-k --insecure Proceed and operate even for server
connections otherwise considered insecure
-v --version Prints the version of this command
```

### Create new service

```shell
$ 3scale import openapi -d <destination> <openapi_resource>
```

### Update existing service

`SERVICE_ID` is required.

```shell
$ 3scale import openapi --service <SERVICE_ID> -d <destination> <openapi_resource>
```

### OpenAPI definition from filename in path

Allowed formats are `json` and `yaml`. Format is automatically detected from filename __extension__.

```shell
$ 3scale import openapi -d <destination> /path/to/your/spec/file.[json|yaml|yml]
```

### OpenAPI definition from URI

Allowed formats are `json` and `yaml`. Format is automatically detected from URL's path __extension__.

```shell
$ 3scale import openapi -d <destination> http[s]://domain/resource/path.[json|yaml|yml]
```

### OpenAPI definition from stdin

Command line parameter for the openapi resource is `-`.

Allowed formats are `json` and `yaml`. Format is automatically detected internally with parsers.

```shell
$ tool_to_read_openapi_from_source | 3scale import openapi -d <destination> -
```
10 changes: 9 additions & 1 deletion exe/3scale
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#!/usr/bin/env ruby

require '3scale_toolbox'
def suppress_warnings
original_verbosity = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = original_verbosity
end

suppress_warnings { require '3scale_toolbox' }

args = ARGV.clone

Expand Down
1 change: 1 addition & 0 deletions lib/3scale_toolbox/commands/copy_command/copy_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def run
source_service = Entities::Service.new(id: arguments[:service_id],
remote: threescale_client(source))
target_service = create_new_service(source_service.show_service, destination, system_name)
puts "new service id #{target_service.id}"
context = create_context(source_service, target_service)
tasks = [
Tasks::CopyServiceProxyTask.new(context),
Expand Down
2 changes: 2 additions & 0 deletions lib/3scale_toolbox/commands/import_command.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'cri'
require '3scale_toolbox/base_command'
require '3scale_toolbox/commands/import_command/import_csv'
require '3scale_toolbox/commands/import_command/openapi'

module ThreeScaleToolbox
module Commands
Expand All @@ -15,6 +16,7 @@ def self.command
end
end
add_subcommand(ImportCsvSubcommand)
add_subcommand(OpenAPI::OpenAPISubcommand)
end
end
end
70 changes: 70 additions & 0 deletions lib/3scale_toolbox/commands/import_command/openapi.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require 'swagger'
require '3scale_toolbox/commands/import_command/openapi/method'
require '3scale_toolbox/commands/import_command/openapi/mapping_rule'
require '3scale_toolbox/commands/import_command/openapi/operation'
require '3scale_toolbox/commands/import_command/openapi/step'
require '3scale_toolbox/commands/import_command/openapi/resource_reader'
require '3scale_toolbox/commands/import_command/openapi/threescale_api_spec'
require '3scale_toolbox/commands/import_command/openapi/create_method_step'
require '3scale_toolbox/commands/import_command/openapi/create_mapping_rule_step'
require '3scale_toolbox/commands/import_command/openapi/create_service_step'

module ThreeScaleToolbox
module Commands
module ImportCommand
module OpenAPI
class OpenAPISubcommand < Cri::CommandRunner
include ThreeScaleToolbox::Command
include ResourceReader

def self.command
Cri::Command.define do
name 'openapi'
usage 'openapi [opts] -d <dst> <spec>'
summary 'Import API defintion in OpenAPI specification'
description 'Using an API definition format like OpenAPI, import to your 3scale API'

option :d, :destination, '3scale target instance. Format: "http[s]://<authentication>@3scale_domain"', argument: :required
option :t, 'target_system_name', 'Target system name', argument: :required
param :openapi_resource

runner OpenAPISubcommand
end
end

def run
context = create_context

tasks = []
tasks << CreateServiceStep.new(context)
tasks << CreateMethodsStep.new(context)
tasks << ThreeScaleToolbox::Tasks::DestroyMappingRulesTask.new(context)
tasks << CreateMappingRulesStep.new(context)

# run tasks
tasks.each(&:call)
end

private

def create_context
{
api_spec: ThreeScaleApiSpec.new(load_openapi),
threescale_client: threescale_client(fetch_required_option(:destination)),
target_system_name: options[:target_system_name]
}
end

def load_openapi
Swagger.build(load_resource(arguments[:openapi_resource]))
# Disable validation step because https://petstore.swagger.io/v2/swagger.json
# does not pass validation. Maybe library's schema is outdated?
# openapi.tap(&:validate)
rescue Swagger::InvalidDefinition, Hashie::CoercionError, Psych::SyntaxError => e
raise ThreeScaleToolbox::Error, "OpenAPI schema validation failed: #{e.message}"
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module ThreeScaleToolbox
module Commands
module ImportCommand
module OpenAPI
class CreateMappingRulesStep
include Step

def call
operations.each do |op|
service.create_mapping_rule(op.mapping_rule)
puts "Created #{op.http_method} #{op.pattern} endpoint"
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module ThreeScaleToolbox
module Commands
module ImportCommand
module OpenAPI
class CreateMethodsStep
include Step

def call
hits_metric_id = service.hits['id']
operations.each do |op|
res = service.create_method(hits_metric_id, op.method)
metric_id = res['id']
# if method system_name exists, ignore error and get metric_id
# Make operation indempotent
unless res['errors'].nil?
if !res['errors']['system_name'].nil? \
&& res['errors']['system_name'][0] =~ /has already been taken/
metric_id = method_id_by_system_name[op.method['system_name']]
else
raise Error, "Metohd has not been saved. Errors: #{res['errors']}"
end
end

op.set(:metric_id, metric_id)
end
end

private

def method_id_by_system_name
@method_id_by_system_name ||= service.methods.each_with_object({}) do |method, acc|
acc[method['system_name']] = method['id']
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module ThreeScaleToolbox
module Commands
module ImportCommand
module OpenAPI
class CreateServiceStep
include Step

##
# Creates service with a given system_name
# If service already exists, update basic settings like name and description
def call
# Create service and update context
self.service = Entities::Service.create(remote: threescale_client,
service: service_settings,
system_name: service_system_name)
puts "Created service id: #{service.id}, name: #{service_name}"
rescue ThreeScaleToolbox::Error => e
raise unless e.message =~ /"system_name"=>\["has already been taken"\]/

# Update service and update context
self.service = Entities::Service.new(id: service_id, remote: threescale_client)
service.update_service(service_settings)
puts "Updated service id: #{service.id}, name: #{service_name}"
end

private

def service_system_name
target_system_name || service_name.downcase.tr(' ', '_')
end

def service_id
@service_id ||= fetch_service_id
end

def fetch_service_id
# figure out service by system_name
service_found = threescale_client.list_services.find do |svc|
svc['system_name'] == service_system_name
end
# It should exist
raise ThreeScaleToolbox::Error, "Service with system_name: #{service_system_name}, should exist" if service_found.nil?

service_found['id']
end

def service_settings
default_service_settings.tap do |svc|
svc['name'] = service_name
svc['description'] = service_description
end
end

def default_service_settings
{}
end

def service_name
api_spec.title
end

def service_description
api_spec.description
end
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/3scale_toolbox/commands/import_command/openapi/mapping_rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module ThreeScaleToolbox
module Commands
module ImportCommand
module OpenAPI
module MappingRule
def mapping_rule
{
'pattern' => pattern,
'http_method' => http_method,
'delta' => delta,
'metric_id' => metric_id
}
end

def http_method
operation[:verb].upcase
end

def pattern
# apply strict matching
operation[:path] + '$'
end

def delta
1
end

def metric_id
operation[:metric_id]
end
end
end
end
end
end
Loading

0 comments on commit d02f2cb

Please sign in to comment.