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

Create OpenAPI spec for CloudLaunch #109

Closed
afgane opened this issue Oct 22, 2017 · 19 comments
Closed

Create OpenAPI spec for CloudLaunch #109

afgane opened this issue Oct 22, 2017 · 19 comments

Comments

@afgane
Copy link
Contributor

afgane commented Oct 22, 2017

CloudLaunch exposes a REST API for manipulating IaaS infrastructure and CloudLaunch-specific appliance management functionality that would be great to have language-specific bindings for. OpenAPI spec, probably using swagger.io tools, seems like the best way to go about generating the bindings.

@machristie
Copy link
Collaborator

I'm currently working on this.

@machristie
Copy link
Collaborator

machristie commented Nov 2, 2017

TODO:

  • Add a schema view
  • Create an OpenAPI/Swagger renderer and update schema view to use this renderer
  • Does authentication need any special handling?
  • Generate code with AutoRest
  • Evaluate drf_openapi as a possible alternative to django-rest-swagger
  • customize the drf_openapi generated schema: allow anyone to view and customize the name
  • try running AutoRest on the drf_openapi generated schema
  • Customize the schema rendering so that instances are Documents and can be acted upon?
  • try swagger-codegen to generate an api client
  • enable coreapi schema
  • use coreapi python client to develop a simple command line interface to the CloudLaunch REST API
  • how to specify a config file as an input (launch config) - prototype by launching a deployment
  • how to get an authentication token when logging in with OpenID Connect? See issue Support getting an auth token when logging in through OpenID Connect #125
  • save the authentication token in a config file for use with the command line client

@bgruening
Copy link
Member

@machristie this is so cool! Can you please document what you are doing. We might want to do the same for whole Galaxy and you experience can be priceless!

@nuwang
Copy link
Member

nuwang commented Nov 2, 2017

Moved from: #95
With Django Rest Framework Swagger, we should be able to generate an OpenAPI spec for the current API. We can then use that spec to generate client bindings for multiple languages.

Additional references:
https://apievangelist.com/2015/06/06/comparison-of-automatic-api-code-generation-tools-for-swagger/

@machristie
Copy link
Collaborator

@bgruening will do!

@nuwang thanks for these resources! Looks like I don't need to create a Swagger renderer after all.

machristie added a commit to machristie/cloudlaunch that referenced this issue Nov 2, 2017
@machristie
Copy link
Collaborator

Using the simple openapi client

Following the OpenAPI/Swagger instructions on this page.

Also Django REST Framework docs

Create a new virtualenv for the client

BL-UITS-RTLT021:cloudlaunch machrist$ python3 -m venv ../coreapi-cli-env
BL-UITS-RTLT021:cloudlaunch machrist$ source ../coreapi-cli-env/bin/ac
activate       activate.csh   activate.fish
BL-UITS-RTLT021:cloudlaunch machrist$ source ../coreapi-cli-env/bin/activate
(coreapi-cli-env) BL-UITS-RTLT021:cloudlaunch machrist$ pip install coreapi-cli
Collecting coreapi-cli
  Downloading coreapi-cli-1.0.6.tar.gz
Collecting coreapi>=1.32.0 (from coreapi-cli)
  Using cached coreapi-2.3.3-py2.py3-none-any.whl
Collecting click>=6.0 (from coreapi-cli)
  Using cached click-6.7-py2.py3-none-any.whl
Collecting coreschema (from coreapi>=1.32.0->coreapi-cli)

Collecting uritemplate (from coreapi>=1.32.0->coreapi-cli)
  Using cached uritemplate-3.0.0-py2.py3-none-any.whl
Collecting requests (from coreapi>=1.32.0->coreapi-cli)
  Using cached requests-2.18.4-py2.py3-none-any.whl
Collecting itypes (from coreapi>=1.32.0->coreapi-cli)
Collecting jinja2 (from coreschema->coreapi>=1.32.0->coreapi-cli)
  Using cached Jinja2-2.9.6-py2.py3-none-any.whl
Collecting idna<2.7,>=2.5 (from requests->coreapi>=1.32.0->coreapi-cli)
  Using cached idna-2.6-py2.py3-none-any.whl
Collecting urllib3<1.23,>=1.21.1 (from requests->coreapi>=1.32.0->coreapi-cli)
  Using cached urllib3-1.22-py2.py3-none-any.whl
Collecting chardet<3.1.0,>=3.0.2 (from requests->coreapi>=1.32.0->coreapi-cli)
  Using cached chardet-3.0.4-py2.py3-none-any.whl
Collecting certifi>=2017.4.17 (from requests->coreapi>=1.32.0->coreapi-cli)
  Using cached certifi-2017.7.27.1-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from jinja2->coreschema->coreapi>=1.32.0->coreapi-cli)
  Using cached MarkupSafe-1.0.tar.gz
Installing collected packages: MarkupSafe, jinja2, coreschema, uritemplate, idna, urllib3, chardet, certifi, requests, itypes, coreapi, click, coreapi-cli
  Running setup.py install for MarkupSafe ... done
  Running setup.py install for coreapi-cli ... done
Successfully installed MarkupSafe-1.0 certifi-2017.7.27.1 chardet-3.0.4 click-6.7 coreapi-2.3.3 coreapi-cli-1.0.6 coreschema-0.0.4 idna-2.6 itypes-1.1.0 jinja2-2.9.6 requests-2.18.4 uritemplate-3.0.0 urlli
b3-1.22
(coreapi-cli-env) BL-UITS-RTLT021:cloudlaunch machrist$ pip install openapi-codec
Collecting openapi-codec
Requirement already satisfied: coreapi>=2.2.0 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from openapi-codec)
Requirement already satisfied: coreschema in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: uritemplate in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: requests in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: itypes in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: jinja2 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from coreschema->coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: certifi>=2017.4.17 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from requests->coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from requests->coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: urllib3<1.23,>=1.21.1 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from requests->coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: idna<2.7,>=2.5 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from requests->coreapi>=2.2.0->openapi-codec)
Requirement already satisfied: MarkupSafe>=0.23 in /Users/machrist/SGCI/cloudlaunch/launcher/coreapi-cli-env/lib/python3.6/site-packages (from jinja2->coreschema->coreapi>=2.2.0->openapi-codec)
Installing collected packages: openapi-codec
Successfully installed openapi-codec-1.3.2

Getting an application list

(coreapi-cli-env) BL-UITS-RTLT021:cloudlaunch machrist$ coreapi get http://localhost:8000/api/v1/schema?format=openapi
<CloudLaunch API "http://localhost:8000/api/v1/schema/?format=openapi">
    applications: {
        list([page], [page_size], [search])
        create(name, [status], [summary], [maintainer], [description], [info_url], [icon_url])
        read(slug)
        update(slug, name, [status], [summary], [maintainer], [description], [info_url], [icon_url])
        partial_update(slug, [name], [status], [summary], [maintainer], [description], [info_url], [icon_url])
        delete(slug)
    }
    auth: {
        list()

        login_create(password, [username], [email])
        logout_list()
        logout_create()
        password_reset_create(email)
        password_reset_confirm_create(new_password1, new_password2, uid, token)
        registration_create(username, password1, password2, [email])
        registrationverify-email_create(key)
        user_credentials_aws_list([page])
        user_credentials_aws_create(cloud_id, name, access_key, [default], [secret_key])
        user_credentials_aws_read(id)
        user_credentials_aws_update(id, cloud_id, name, access_key, [default], [secret_key])
        user_credentials_aws_partial_update(id, [cloud_id], [name], [default], [access_key], [secret_key])
        user_credentials_aws_delete(id)
        user_credentials_azure_list([page])
        user_credentials_azure_create(cloud_id, name, subscription_id, client_id, [default], [secret], [tenant])
        user_credentials_azure_read(id)
        user_credentials_azure_update(id, cloud_id, name, subscription_id, client_id, [default], [secret], [tenant])
        user_credentials_azure_partial_update(id, [cloud_id], [name], [default], [subscription_id], [client_id], [secret], [tenant])
        user_credentials_azure_delete(id)
        user_credentials_gce_list([page])
        user_credentials_gce_create(credentials, cloud_id, name, [default])
        user_credentials_gce_read(id)
        user_credentials_gce_update(id, credentials, cloud_id, name, [default])
        user_credentials_gce_partial_update(id, [credentials], [cloud_id], [name], [default])
        user_credentials_gce_delete(id)
        user_credentials_openstack_list([page])
        user_credentials_openstack_create(cloud_id, name, username, project_name, [default], [password], [project_domain_name], [user_domain_name])
        user_credentials_openstack_read(id)
        user_credentials_openstack_update(id, cloud_id, name, username, project_name, [default], [password], [project_domain_name], [user_domain_name])
        user_credentials_openstack_partial_update(id, [cloud_id], [name], [default], [username], [password], [project_name], [project_domain_name], [user_domain_name])
        user_credentials_openstack_delete(id)
    }
    cors_proxy: {
        list()
    }
...
(coreapi-cli-env) BL-UITS-RTLT021:cloudlaunch machrist$ coreapi action applications list
{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://localhost:8000/api/v1/applications/ubuntu/",
            "slug": "ubuntu",
            "versions": [

                {
                    "version": "16.04",
                    "cloud_config": [
                        {
                            "cloud": {
                                "slug": "amazon-us-east-n-virginia",
                                "compute": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/compute/",
                                "security": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/security/",
                                "storage": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/storage/",
                                "networks": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/networks/",
                                "static_ips": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/static_ips/",
                                "region_name": "us-east-1",
                                "cloud_type": "aws",
                                "extra_data": {
                                    "ec2_region_name": "us-east-1",
                                    "ec2_region_endpoint": "ec2.amazonaws.com",
                                    "ec2_conn_path": "/",
                                    "ec2_port": null,
                                    "ec2_is_secure": false,
                                    "s3_host": "s3.amazonaws.com",
                                    "s3_conn_path": "/",
                                    "s3_port": null,
                                    "s3_is_secure": true
                                },
                                "added": "2016-06-26T03:08:59.048000Z",
                                "updated": "2017-04-21T14:30:15.694000Z",
                                "name": "Amazon US East - N. Virginia",
                                "access_instructions_url": "https://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key"
...

Issues:

  • deployments isn't listed although it is in the swagger UI page
  • not sure how to read an application. This coreapi action applications read --params slug=ubuntu doesn't work.
  • Looking at coreapi, the auto generated schema seems really simple. We might need to customize it or go full manual. For example, there is just a single "Document" that is returned.
  • haven't figured out how to authenticate yet. This coreapi credentials add --auth basic localhost admin:PASSWORD didn't work

@nuwang
Copy link
Member

nuwang commented Nov 3, 2017

@machristie This is looking great! Did you have to annotate things manually using CoreAPI, or did it pick up most of the required data from DRF automatically?

Also, is there any advantage in using django-rest-swagger or does that generate the Swagger web UI only?

@machristie
Copy link
Collaborator

@nuwang As a first step I didn't do any manual annotating or anything, just to see what is available out of the box. This is all automatically generated at this time. You can see in this commit that there's almost nothing to it. However, as I mentioned above, this is probably not sufficient.

django-rest-swagger generates the Swagger web UI but also the schema document. So that seems like a good advantage to using it, otherwise we'd need to implement it.

@nuwang
Copy link
Member

nuwang commented Nov 3, 2017

Ok, that's good to hear.

basic auth was explicitly disabled, as I recall it was either because it was not particularly secure when not using https, or because it kept popping up the dialogue or maybe both.
https://github.com/galaxyproject/cloudlaunch/blob/dev/django-cloudlaunch/cloudlaunchserver/settings.py#L217

However, it looks like token auth should work? coreapi credentials add localhost "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

@machristie
Copy link
Collaborator

machristie commented Nov 6, 2017

There is a bug preventing the setting of the Authorization header in coreapi-cli. To workaround this I downgraded coreapi to version 2.2.4

pip install coreapi==2.2.4

Here are the steps to authenticate with the command line client:

  1. Log in with username and password using the form at http://localhost:8000/api/v1/auth/login/
  2. Take the token and run the following command:
coreapi credentials add localhost "Token 846dd0e4bbdf...."
  1. Now I can make calls and get back a user's saved credentials
$ coreapi action -d auth user_credentials_aws_list
> GET /api/v1/auth/user/credentials/aws/ HTTP/1.1
> Accept-Encoding: gzip, deflate
> Connection: keep-alive
> Accept: application/coreapi+json, application/vnd.coreapi+json, */*
> Authorization: Token 846dd0e4bbdf....
> Host: localhost

> User-Agent: coreapi
< 200 OK
< Allow: GET, POST, HEAD, OPTIONS
< Content-Length: 1269
< Content-Type: application/json
< Date: Mon, 06 Nov 2017 14:24:56 GMT
< Server: WSGIServer/0.2 CPython/3.6.0
< Vary: Accept, Cookie
< X-Frame-Options: SAMEORIGIN
<
< {"count":1,"next":null,"previous":null,"results":[{"url":...

Note: the -d flag shows the HTTP headers in the request and response. I used this to confirm that the Authorization header is being sent.

Also: with auth configured I'm now seeing deployments in the returned schema document.

@machristie
Copy link
Collaborator

There's a typo in the coreapi docs: there is no --params argument, instead it is --param or -p. I can use the following to read a specific deployment

coreapi action deployments read -p id=1

machristie added a commit to machristie/cloudlaunch that referenced this issue Nov 7, 2017
@machristie
Copy link
Collaborator

machristie commented Nov 7, 2017

As I discussed with Nuwan today, it would be nice if the schema was aware of the hierarchy of REST URLs, like these Core API examples. For example, you could get a deployment and that becomes your context and from there you can get a list of tasks, etc. for that deployment.

As you can see above, the generated schema is flat. django-rest-swagger generates the openapi/swagger schema based the coreapi autogenerated schema provided by django-rest-framework. One option might be to manually specify a coreapi schema that utilizes nested documents. However, even if we did that I'm not sure how that would be rendered by swagger since swagger seems unable to model hierarchies of URLs.

We also probably don't want the overhead of manually specifying schemas in the first place. I'm going to investigate code generation and see what generated client code looks like and then decide whether it makes sense to customize the schema.

@machristie
Copy link
Collaborator

Generating code with AutoRest

Basically following these instructions.

mkdir node_modules
npm i autorest

This fails because I need a more recent version of node. I downloaded 8.9.1 LTS release from https://nodejs.org/en/.

Now I can install autorest

npm i autorest

Save the schema document to a file:

curl 'http://localhost:8000/api/v1/schema/?format=openapi' -XGET -H 'Referer: http://localhost:8000/api/v1/schema/' -H 'Host: localhost:8000' -H 'Accept: application
/json;charset=utf-8,*/*' -H 'Connection: keep-alive' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-us' -H 'DNT: 1' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel
 Mac OS X 10_12_6) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5' -H 'Cookie: csrftoken=BX3Jj6DIw9dEN4Qx2mj7V7yesIxrckUZxu2aDG4XRr5ytyPCk9lImx1ojXNnZZ
pc; _xsrf=2|10e974b2|0e96d3020941a258b1426d829d2e834b|1508766834; username-localhost-8888="2|1:0|10:1508766834|23:username-localhost-8888|44:MDE3NjEyMDlmZTEzNGQ5NzljNDUyZjk5M
WJiZmNhZTA=|a126a1d9050ae27e1c520413fa711173a95ba465ef69ba2317ae71213a644724"' > schema.json

Try to generate Python code

$ ./node_modules/.bin/autorest --input-file=schema.json --output-folder=./generated/code/ --namespace=cloudlaunch --python
AutoRest code generation utility [version: 2.0.4166]

(C) 2017 Microsoft Corporation.
https://aka.ms/autorest
No configuration found at 'file:///Users/machrist/SGCI/cloudlaunch/launcher/cloudlaunch/'.
Loading AutoRest extension '@microsoft.azure/autorest.python' (~2.0.0)
Loading AutoRest extension '@microsoft.azure/autorest.modeler' (~2.0.0)
/bin/sh: /Users/machrist/.autorest/@[email protected]/node_modules/.bin/dotnet: Permission denied
Process() Cancelled due to exception : [Exception] AutoRest extension '@microsoft.azure/autorest.modeler' terminated.

This appears to fail because the call.js file in the dotnet node module is not executable. Luckily this fixes it

$ chmod +x ~/.autorest/@[email protected]/node_modules/dotnet-2.0.0/dist/call.js

Now I can generate Python code

./node_modules/.bin/autorest --input-file=schema.json --output-folder=./generated/code/ --namespace=cloudlaunch --python

The generated code looks pretty weird. Maybe the schema needs some work or maybe options need to be passed to AutoRest. There are two immediate problems I see:

  • all operation methods are named some variant of list, create, update, read, delete, but where there is more than onelist operation you get list1, list2, etc. For example, two "list" methods in the AuthOperation class:
    def list(
            self, custom_headers=None, raw=False, **operation_config):
        """List authentication endpoints.

        List authentication endpoints.

        :param dict custom_headers: headers that will be added to the request
        :param bool raw: returns the direct response alongside the
         deserialized response
        :param operation_config: :ref:`Operation configuration
         overrides<msrest:optionsforoperations>`.
        :return: None or
         :class:`ClientRawResponse<msrest.pipeline.ClientRawResponse>` if
         raw=true
        :rtype: None or
         :class:`ClientRawResponse<msrest.pipeline.ClientRawResponse>`
        :raises:
         :class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
        """
        # Construct URL
        url = '/api/v1/auth/'
...
    def list1(
            self, custom_headers=None, raw=False, **operation_config):
        """Calls Django logout method and delete the Token object.

        Calls Django logout method and delete the Token object
        assigned to the current User object.
        Accepts/Returns nothing.

        :param dict custom_headers: headers that will be added to the request
        :param bool raw: returns the direct response alongside the
         deserialized response
        :param operation_config: :ref:`Operation configuration
         overrides<msrest:optionsforoperations>`.
        :return: None or
         :class:`ClientRawResponse<msrest.pipeline.ClientRawResponse>` if
         raw=true
        :rtype: None or
         :class:`ClientRawResponse<msrest.pipeline.ClientRawResponse>`
        :raises:
         :class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
        """
        # Construct URL
        url = '/api/v1/auth/logout/'
...
  • all model classes are named like Data, DataModel, DataModelModel, etc., all the ways up to the comically long DataModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModelModel

@machristie
Copy link
Collaborator

Trying out drf_openapi

Installed drf_openapi and following the drf_openapi quickstart guide.

  • drf_openapi requires API versioning and supports URLPathVersioning. Added URLPathVersioning to REST_FRAMEWORK config
   'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',

and changed API prefix to have a named capturing group named version

API_PREFIX = r'api/(?P<version>v1)/'
  • Unfortunately the latest version of drf_openapi (1.0.0) has a bug
[2017-11-20 14:50:56,729 django.request:135 ERROR] Internal Server Error: /api/v1/schema/
Traceback (most recent call last):
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 217, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 215, in _get_response
    response = response.render()
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/django/template/response.py", line 107, in render
    self.content = self.rendered_content
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/rest_framework/response.py", line 72, in rendered_content
    ret = renderer.render(self.data, accepted_media_type, context)
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/drf_openapi/codec.py", line 113, in render
    return OpenAPICodec().encode(data, extra=extra)
  File "/Users/machrist/SGCI/cloudlaunch/launcher/venv/lib/python3.6/site-packages/drf_openapi/codec.py", line 103, in encode
    return force_bytes(json.dumps(data))
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/encoder.py", line 180, in default
    o.__class__.__name__)
TypeError: Object of type '__proxy__' is not JSON serializable

To fix I had to install from source.

  • drf_openapi only works with DRF 3.7 (location of schema classes has changed in DRF 3.7). That required some additional small changes.

Once I had it setup, here's what it looks like when interacted with via coreapi client

$ coreapi get http://localhost:8000/api/v1/schema?format=openapi
<My custom API schema title "http://localhost:8000/api/v1/schema/?format=openapi">
    api: {
        applications_list([page], [page_size], [search])
        applications_create(name, [status], [summary], [maintainer], [description], [info_url], [icon_url])
        applications_read(slug, [search])
        applications_update(slug, name, [search], [status], [summary], [maintainer], [description], [info_url], [icon_url])
        applications_partial_update(slug, [search], [name], [status], [summary], [maintainer], [description], [info_url], [icon_url])
        applications_delete(slug, [search])
        auth_list()
        auth_login_create(password, [username], [email])
        auth_logout_list()
        auth_logout_create()
        auth_password_change_create(new_password1, new_password2)

        auth_password_reset_create(email)
        auth_password_reset_confirm_create(new_password1, new_password2, uid, token)
        auth_registration_create(username, password1, password2, [email])
        auth_registrationverify-email_create()
        auth_user_read()
        auth_user_update(username, [first_name], [last_name])
        auth_user_partial_update([username], [first_name], [last_name])
        auth_user_credentials_list([page])
        auth_user_credentials_aws_list([page])
        auth_user_credentials_aws_create(cloud_id, name, access_key, [default], [secret_key])
        auth_user_credentials_aws_read(id)
        auth_user_credentials_aws_update(id, cloud_id, name, access_key, [default], [secret_key])
        auth_user_credentials_aws_partial_update(id, [cloud_id], [name], [default], [access_key], [secret_key])
        auth_user_credentials_aws_delete(id)
        auth_user_credentials_azure_list([page])
        auth_user_credentials_azure_create(cloud_id, name, subscription_id, client_id, [default], [secret], [tenant])
        auth_user_credentials_azure_read(id)
        auth_user_credentials_azure_update(id, cloud_id, name, subscription_id, client_id, [default], [secret], [tenant])
        auth_user_credentials_azure_partial_update(id, [cloud_id], [name], [default], [subscription_id], [client_id], [secret], [tenant])
        auth_user_credentials_azure_delete(id)
        auth_user_credentials_gce_list([page])
        auth_user_credentials_gce_create(credentials, cloud_id, name, [default])
        auth_user_credentials_gce_read(id)
        auth_user_credentials_gce_update(id, credentials, cloud_id, name, [default])
        auth_user_credentials_gce_partial_update(id, [credentials], [cloud_id], [name], [default])
        auth_user_credentials_gce_delete(id)
        auth_user_credentials_openstack_list([page])
        auth_user_credentials_openstack_create(cloud_id, name, username, project_name, [default], [password], [project_domain_name], [user_domain_name])
        auth_user_credentials_openstack_read(id)
        auth_user_credentials_openstack_update(id, cloud_id, name, username, project_name, [default], [password], [project_domain_name], [user_domain_name
])
        auth_user_credentials_openstack_partial_update(id, [cloud_id], [name], [default], [username], [password], [project_name], [project_domain_name], [
user_domain_name])
        auth_user_credentials_openstack_delete(id)
        deployments_list([page], [ordering], [archived])
        deployments_create(name, application_version, target_cloud, [application], [config_app], [archived])
        deployments_tasks_list(deployment_pk, [page], [ordering])
...

So this schema is even flatter than the one produced by django-rest-swagger, although in practice it doesn't make much difference. Here I get a list of applications:

$ coreapi action api applications_list
{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [

        {
            "url": "http://localhost:8000/api/v1/applications/ubuntu/",
            "slug": "ubuntu",
            "versions": [
                {
                    "version": "16.04",
                    "cloud_config": [
                        {
                            "cloud": {
                                "slug": "amazon-us-east-n-virginia",
                                "compute": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/compute/",
                                "security": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/security/",
                                "storage": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/storage/",
                                "networks": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/networks/",
                                "static_ips": "http://localhost:8000/api/v1/infrastructure/clouds/amazon-us-east-n-virginia/static_ips/",
                                "region_name": "us-east-1",
                                "cloud_type": "aws",
                                "extra_data": {
                                    "ec2_region_name": "us-east-1",
                                    "ec2_region_endpoint": "ec2.amazonaws.com",
                                    "ec2_conn_path": "/",
                                    "ec2_port": null,
                                    "ec2_is_secure": false,
                                    "s3_host": "s3.amazonaws.com",
                                    "s3_conn_path": "/",
                                    "s3_port": null,
                                    "s3_is_secure": true
                                },
                                "added": "2016-06-25T23:08:59.048000-04:00",
                                "updated": "2017-04-21T10:30:15.694000-04:00",
                                "name": "Amazon US East - N. Virginia",
                                "access_instructions_url": "https://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key"
                            },
                            "image": {
...

Other observations:

machristie added a commit to machristie/cloudlaunch that referenced this issue Nov 20, 2017
Ran into a bug in 1.0.0 drf_openapi and had to install from source. This
also required upgrading Django to 3.7.3 (different schema module
layout). Also drf_openapi requires URLPathVersioning which I enabled.
@machristie
Copy link
Collaborator

Trying out AutoRest against drf_openapi schema output

$ ./node_modules/.bin/autorest --input-file=schema_drf_openapi.json --output-folder=./generated/code/ --namespace=cloudlaunch --python
AutoRest code generation utility [version: 2.0.4166]

(C) 2017 Microsoft Corporation.
https://aka.ms/autorest
No configuration found at 'file:///Users/machrist/SGCI/cloudlaunch/launcher/cloudlaunch/'.
Loading AutoRest extension '@microsoft.azure/autorest.python' (~2.0.0)
Loading AutoRest extension '@microsoft.azure/autorest.modeler' (~2.0.0)
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.versions.required', line 14, position 23.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.

ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.vm_firewall_ids.required', line 26, position 22.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.public_ips.required', line 18, position 23.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.vm_firewall_ids.required', line 26, position 22.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.vm_firewall_ids.required', line 26, position 23.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.sponsors.required', line 38, position 23.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.sponsors.required', line 50, position 23.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.sponsors.required', line 38, position 23.
ERROR: Error converting value False to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.sponsors.required', line 38, position 23.
ERROR: Error converting value True to type 'System.Collections.Generic.IList`1[System.String]'. Path 'properties.results.required', line 6, position 22.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Zero' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
WARNING: Method 'Read' does not declare any MIME type for the return body. Generated code will not deserialize the content.
{ Error: ENAMETOOLONG: name too long, open '/Users/machrist/SGCI/cloudlaunch/launcher/cloudlaunch/generated/code/mycustom_ap_ischematitle/models/data_model_model_model_model_model_model_model_model_model_model_model_model_model_model_m
odel_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model.py'
    at Object.fs.openSync (fs.js:646: 18)
    at Object.fs.writeFileSync (fs.js:1291: 33)
    at Object.exports.writeFile (/Users/machrist/.autorest/@[email protected]/node_modules/@microsoft.azure/autorest-core/dist/lib/ref/async.js:14: 63)
    at WriteStringInternal (/Users/machrist/.autorest/@[email protected]/node_modules/@microsoft.azure/autorest-core/dist/lib/ref/uri.js:260: 19)
    at <anonymous>
  errno: -63,
  code: 'ENAMETOOLONG',
  syscall: 'open',
  path: '/Users/machrist/SGCI/cloudlaunch/launcher/cloudlaunch/generated/code/mycustom_ap_ischematitle/models/data_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_
model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model_model.py' }

This produces similarly bad results, although it seems to fail halfway through. It doesn't generate any operations code, only models.

Somewhere along the way it seems like the names of the models are getting lost or maybe they need to be specified somehow. I'm trying to figure out if that is possible.

@machristie
Copy link
Collaborator

machristie commented Nov 28, 2017

Summarizing discussion with Nuwan and Enis:

  • AutoRest seems to assume that requests and responses will have defined schemas for requests and responses (defined in the definitions section of openapi schema doc). drf_openapi and django-rest-swagger don't infer these and instead the request and response is of type object.
  • overall, it seems like these OpenAPI plugins to DRF are not good for generating schemas that are useful for generating client code. Their main use case seems to be to generate documentation viewers. This is infact how they describe themselves, as documentation generators.
  • there are fundamental limitations to trying to create a OpenAPI schema on top of the DRF generated CoreAPI schema. DRF team is working to make it easier for plugins to be developed that generate alternate schema formats directly instead of on top of CoreAPI. See especially this comment, specifically:

To this goal, Django REST Framework version 3.7 introduced the per-view schema customisation. My expectation here is that someone will:

  1. Implement a version of AutoSchema that skips CoreAPI and generates OpenAPI, including response schemas, directly.
  2. Implement a version of SchemaGenerator that collates OpenAPI schemas rather than CoreAPI Documents
  3. Publish that as a third-party package targeting the OpenAPI+DRF demographic ( i.e. the people here).
  • The best way forward at this point would be to just use CoreAPI. The goal is to create a command line client to the REST API, so this week I'm going to see if the CoreAPI schema and python client can be used to help create a command line client.

@machristie
Copy link
Collaborator

machristie commented Dec 1, 2017

I've been able to integrate the coreapi schema and then write a simple Python script to use the coreapi client and list a user's AWS credentials:

from coreapi import Client
from coreapi import transports

auth_token = '...'
transports = [
    transports.HTTPTransport(credentials={'localhost': 'Token {}'.format(auth_token)})
]
client = Client(transports=transports)

document = client.get('http://localhost:8000/api/v1/schema/')

aws_credentials_list = client.action(document, ['auth', 'user', 'credentials', 'aws', 'list'])
print(aws_credentials_list)

With this as a staring point it should make developing a command line client much simpler. Still to do are to see how to handle specifying a launch config file and how that works.

One additional note: I found a good working version for coreapi is 2.2.3. Later ones run into two bugs:

machristie added a commit to machristie/cloudlaunch that referenced this issue Dec 1, 2017
nuwang added a commit that referenced this issue Dec 18, 2017
@machristie
Copy link
Collaborator

@afgane This issue can be closed now.

There some follow on work still on-going:

@afgane
Copy link
Contributor Author

afgane commented Dec 19, 2017

Thanks for seeing this through, and the summary of the follow-up tasks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants