Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import OpenAPI v2.0 command #76

Merged
merged 22 commits into from
Jan 21, 2019
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9937f0b
import openapi command
eguzki Nov 27, 2018
e7d32ca
exe/3scale: suppress warnings from Ruby only for production use, not …
eguzki Dec 7, 2018
1f48898
openapi import: create service if service id not provided
eguzki Dec 7, 2018
724c25a
openapi: handle schema validation errors
eguzki Dec 7, 2018
5300c31
openapi/command: enable oas definition by url, file or stdin
eguzki Dec 7, 2018
b4e119f
openapi: method names from operationId field
eguzki Dec 7, 2018
2b2b46b
openapi: doc
eguzki Dec 10, 2018
bd4ea44
openapi: minor refactor to improve delegation
eguzki Dec 11, 2018
02fb39c
openapi: unitttests
eguzki Dec 11, 2018
49112dc
openapi: refactor threescale_api_spec
eguzki Dec 11, 2018
3989fb8
Online and offline version of OpenAPI import integration tests
eguzki Dec 11, 2018
d337a7c
openapi: remove mapping rules first
eguzki Dec 14, 2018
f84782c
reset back the verbosity
eguzki Dec 14, 2018
ab7f285
3Scale -> 3scale
eguzki Dec 14, 2018
eb12e6a
openapi: target system name optional parameter. If service exists, up…
eguzki Dec 17, 2018
ea3a8ca
openapi: load oas resource before parsing with swagger parser
eguzki Dec 18, 2018
895ec2d
spec: show info about api clients on before block
eguzki Dec 18, 2018
4f3c0d9
rspec: be_subset_of custom matcher
eguzki Dec 18, 2018
bcadd67
openapi: update remote client method
eguzki Dec 18, 2018
d93f0d6
openapi: apply strict matching on mapping rules
eguzki Jan 17, 2019
7da6ebb
tests: use instance_double when appropriate to check sutbbed methods
eguzki Jan 17, 2019
1cacde2
openapi: fix tests
eguzki Jan 18, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -178,6 +179,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`.
eguzki marked this conversation as resolved.
Show resolved Hide resolved
* 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
eguzki marked this conversation as resolved.
Show resolved Hide resolved
end

suppress_warnings { require '3scale_toolbox' }
eguzki marked this conversation as resolved.
Show resolved Hide resolved

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
operation[:path]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the pattern compatible with what mapping rules support?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't checked that. Is it documented what mapping rules support in pattern field? Send link please.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapping rules accepted expression set includes openapi 2.0 path expressions.

The field name MUST begin with a slash and Path Templating is allowed.
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#patterned-fields

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it works.

Defining a path /pets would match /pets/{id} too. Is that desired? Our expressions are not the exact match to the whole string, just matching the beginning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot find path matching spec in OAS 2.0 spec. I just found clarifications on path overlapping issues

From common sense, I would expect OAS 2.0 spec to do exact match. Just a guess.

Since apicast does just prefix matching, I guess we need to specify somehow end of string when creating mapping rule in 3scale. That only if my assumption about exact match on OAS 2.0 is correct. Is there a way to do that? Adding '/' in the end would not work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eguzki it is described in the documentation mentioned in #76 (comment) (using $ at the end)

end

def delta
1
end

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