Skip to content
This repository has been archived by the owner on May 24, 2023. It is now read-only.

Commit

Permalink
Add versioning to API
Browse files Browse the repository at this point in the history
Currently only supported version is /v1.

Signed-off-by: Martin Bašti <mbasti@redhat.com>
  • Loading branch information
MartinBasti authored and csomh committed Feb 27, 2019
1 parent e3f103b commit fcea465
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 78 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ Operator manifests files must be added to zip archive

#### Endpoints

* [POST] `/<organization>/<repository>/zipfile/<version>`
* [POST] `/<organization>/<repository>/zipfile`
* [POST] `/v1/<organization>/<repository>/zipfile/<version>`
* [POST] `/v1/<organization>/<repository>/zipfile`

Zip file must be attached as `content_type='multipart/form-data'` assigned to
field `file`. See `curl` examples bellow.
Expand Down Expand Up @@ -96,20 +96,20 @@ Error messages have following format:

#### Example
```bash
curl -X POST https://example.com/myorg/myrepo/zipfile -F "file=@manifests.zip"
curl -X POST https://example.com/v1/myorg/myrepo/zipfile -F "file=@manifests.zip"
```
or with explicit release version
```bash
curl -X POST https://example.com/myorg/myrepo/zipfile/1.1.5 -F "file=@manifests.zip"
curl -X POST https://example.com/v1/myorg/myrepo/zipfile/1.1.5 -F "file=@manifests.zip"
```

### Removing released operators manifests


#### Endpoints

* [DELETE] `/<organization>/<repository>/<version>`
* [DELETE] `/<organization>/<repository>`
* [DELETE] `/v1/<organization>/<repository>/<version>`
* [DELETE] `/v1/<organization>/<repository>`

If `<version>` is omitted then all released operator manifests are removed
from the specified application repository, but the repository itself will **not** be
Expand Down Expand Up @@ -152,11 +152,11 @@ Error messages have following format:

#### Examples
```bash
curl -X DELETE https://example.com/myorg/myrepo
curl -X DELETE https://example.com/v1/myorg/myrepo
```
or with explicit release version
```bash
curl -X DELETE https://example.com/myorg/myrepo/1.1.5
curl -X DELETE https://example.com/v1/myorg/myrepo/1.1.5
```


Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions omps/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

from flask import Blueprint

API = Blueprint('v1', __name__)

from . import packages, push # register routes
10 changes: 5 additions & 5 deletions omps/packages.py → omps/api/v1/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
#
import logging

from flask import Blueprint, jsonify
from flask import jsonify
import requests

from .quay import QUAY_ORG_MANAGER, ReleaseVersion
from . import API
from omps.quay import QUAY_ORG_MANAGER, ReleaseVersion

logger = logging.getLogger(__name__)
BLUEPRINT = Blueprint('packages', __name__)


@BLUEPRINT.route("/<organization>/<repo>", defaults={'version': None},
@API.route("/<organization>/<repo>", defaults={'version': None},
methods=('DELETE',))
@BLUEPRINT.route("/<organization>/<repo>/<version>", methods=('DELETE',))
@API.route("/<organization>/<repo>/<version>", methods=('DELETE',))
def delete_package_release(organization, repo, version=None):
"""
Delete particular version of released package from quay.io
Expand Down
16 changes: 8 additions & 8 deletions omps/push.py → omps/api/v1/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@
from tempfile import NamedTemporaryFile, TemporaryDirectory
import zipfile

from flask import Blueprint, jsonify, current_app, request
from flask import jsonify, current_app, request

from .constants import (
from . import API
from omps.constants import (
ALLOWED_EXTENSIONS,
DEFAULT_ZIPFILE_MAX_UNCOMPRESSED_SIZE,
)
from .errors import (
from omps.errors import (
OMPSInvalidVersionFormat,
OMPSUploadedFileError,
OMPSExpectedFileError,
QuayPackageNotFound,
)
from .quay import QUAY_ORG_MANAGER, ReleaseVersion
from omps.quay import QUAY_ORG_MANAGER, ReleaseVersion

logger = logging.getLogger(__name__)
BLUEPRINT = Blueprint('push', __name__)


def validate_allowed_extension(filename):
Expand Down Expand Up @@ -109,9 +109,9 @@ def _get_package_version(quay_org, repo, version=None):
return version


@BLUEPRINT.route("/<organization>/<repo>/zipfile", defaults={"version": None},
@API.route("/<organization>/<repo>/zipfile", defaults={"version": None},
methods=('POST',))
@BLUEPRINT.route("/<organization>/<repo>/zipfile/<version>", methods=('POST',))
@API.route("/<organization>/<repo>/zipfile/<version>", methods=('POST',))
def push_zipfile(organization, repo, version=None):
"""
Push the particular version of operator manifest to registry from
Expand Down Expand Up @@ -147,7 +147,7 @@ def push_zipfile(organization, repo, version=None):
return resp


@BLUEPRINT.route("/<organization>/<repo>/koji/<nvr>", methods=('POST',))
@API.route("/<organization>/<repo>/koji/<nvr>", methods=('POST',))
def push_koji_nvr(organization, repo, nvr):
"""
Get operator manifest from koji by specified NVR and upload operator
Expand Down
7 changes: 2 additions & 5 deletions omps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@

from flask import Flask

from .api.v1 import API as API_V1
from .errors import init_errors_handling
from .logger import init_logging
from .packages import BLUEPRINT as PACKAGES_BP
from .push import BLUEPRINT as PUSH_BP
from .settings import init_config
from .quay import QUAY_ORG_MANAGER

Expand Down Expand Up @@ -43,8 +42,6 @@ def _init_errors_handling(app):

def _register_blueprints(app):
logger.debug('Registering blueprints')
app.register_blueprint(PUSH_BP)
app.register_blueprint(PACKAGES_BP)

app.register_blueprint(API_V1, url_prefix='/v1')

app = create_app()
1 change: 1 addition & 0 deletions tests/push/test_api.py → tests/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,4 @@ def test_404_for_mistyped_entrypoints(client, endpoint):
rv_json = rv.get_json()
assert rv_json['error'] == 'NotFound'
assert rv_json['status'] == 404

File renamed without changes.
50 changes: 50 additions & 0 deletions tests/api/v1/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

from collections import namedtuple

import pytest


EntrypointMeta = namedtuple('EntrypointMeta', 'url_path,org,repo,version')


@pytest.fixture(params=[
True, # endpoint with version
False, # endpoint without version
])
def endpoint_push_zipfile(request, release_version):
"""Returns URL for zipfile endpoints"""
organization = 'testorg'
repo = 'repo-Y'
version = release_version if request.param else None

url_path = '/v1/{}/{}/zipfile'.format(organization, repo)
if version:
url_path = '{}/{}'.format(url_path, version)

yield EntrypointMeta(
url_path=url_path, org=organization,
repo=repo, version=version
)


@pytest.fixture(params=[
True, # endpoint with version
False, # endpoint without version
])
def endpoint_packages(request, release_version):
"""Returns URL for packages endpoints"""
organization = 'testorg'
repo = 'repo-Y'

url_path = '/v1/{}/{}'.format(organization, repo)
if request.param:
url_path = '{}/{}'.format(url_path, release_version)

yield EntrypointMeta(
url_path=url_path, org=organization,
repo=repo, version=release_version
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def test_delete_released_package(


@pytest.mark.parametrize('endpoint', [
'/organization-X/repo-Y',
'/organization-X/repo-Y/1.0.1',
'/v1/organization-X/repo-Y',
'/v1/organization-X/repo-Y/1.0.1',
])
@pytest.mark.parametrize('method', [
'GET', 'PATCH' 'PUT', 'HEAD', 'POST', 'TRACE',
Expand All @@ -44,8 +44,9 @@ def test_method_not_allowed(client, endpoint, method):

@pytest.mark.parametrize('endpoint', [
'/',
'/organization-X/',
'/organization-X/repo-Y/version-Z/extra-something',
'/v1',
'/v1/organization-X/',
'/v1/organization-X/repo-Y/version-Z/extra-something',
])
def test_404_for_mistyped_entrypoints(client, endpoint):
"""Test if HTTP 404 is returned for unexpected endpoints"""
Expand Down
117 changes: 117 additions & 0 deletions tests/api/v1/test_api_push.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

from io import BytesIO

import pytest

from omps import constants


def test_push_zipfile(
client, valid_manifests_archive, endpoint_push_zipfile,
mocked_quay_io, mocked_op_courier_push):
"""Test REST API for pushing operators form zipfile"""
with open(valid_manifests_archive, 'rb') as f:
data = {
'file': (f, f.name),
}
rv = client.post(
endpoint_push_zipfile.url_path,
data=data,
content_type='multipart/form-data',
)

assert rv.status_code == 200, rv.get_json()
expected = {
'organization': endpoint_push_zipfile.org,
'repo': endpoint_push_zipfile.repo,
'version': endpoint_push_zipfile.version or constants.DEFAULT_RELEASE_VERSION,
'extracted_files': ['empty.yml'],
}
assert rv.get_json() == expected


@pytest.mark.parametrize('filename', (
'test.json', # test invalid extension
'test.zip', # test invalid content
))
def test_push_zipfile_invalid_file(
client, filename, endpoint_push_zipfile,
mocked_quay_io):
"""Test if proper error is returned when no zip file is being attached"""
data = {
'file': (BytesIO(b'randombytes'), filename),
}
rv = client.post(
endpoint_push_zipfile.url_path,
data=data,
content_type='multipart/form-data',
)

assert rv.status_code == 400, rv.get_json()
rv_json = rv.get_json()
assert rv_json['status'] == 400
assert rv_json['error'] == 'OMPSUploadedFileError'


def test_push_zipfile_no_file(client, endpoint_push_zipfile, mocked_quay_io):
"""Test if proper error is returned when no file is being attached"""
rv = client.post(endpoint_push_zipfile.url_path)
assert rv.status_code == 400, rv.get_json()
rv_json = rv.get_json()
assert rv_json['status'] == 400
assert rv_json['error'] == 'OMPSExpectedFileError'


def test_push_koji_nvr(client):
"""Test REST API for pushing operators form koji by NVR"""
rv = client.post('/v1/organization-X/repo-Y/koji/nvr-Z')
assert rv.status_code == 200
expected = {
'organization': 'organization-X',
'repo': 'repo-Y',
'nvr': 'nvr-Z',
'msg': 'Not Implemented. Testing only'
}
assert rv.get_json() == expected


ZIP_ENDPOINT_NOVER = '/v1/organization-X/repo-Y/zipfile'
@pytest.mark.parametrize('endpoint', [
ZIP_ENDPOINT_NOVER,
'/v1/organization-X/repo-Y/zipfile/1.0.1',
'/v1/organization-X/repo-Y/koji/nvr-Z',
])
@pytest.mark.parametrize('method', [
'GET', 'PATCH' 'PUT', 'HEAD', 'DELETE', 'TRACE',
])
def test_method_not_allowed(client, endpoint, method):
"""Specified endpoints currently support only POST method, test if other
HTTP methods returns proper error code
Method OPTIONS is excluded from testing due its special meaning
"""
if (endpoint, method) == (ZIP_ENDPOINT_NOVER, 'DELETE'):
# accepted, collides with [DELETE] /org/repo/version
return

rv = client.open(endpoint, method=method)
assert rv.status_code == 405


@pytest.mark.parametrize('endpoint', [
'/',
'/v1'
'/v1/organization-X/repo-Y/koji/',
'/v1/organization-X/repo-Y/zipfile/version-Z/extra-something',
])
def test_404_for_mistyped_entrypoints(client, endpoint):
"""Test if HTTP 404 is returned for unexpected endpoints"""
rv = client.post(endpoint)
assert rv.status_code == 404
rv_json = rv.get_json()
assert rv_json['error'] == 'NotFound'
assert rv_json['status'] == 404

0 comments on commit fcea465

Please sign in to comment.