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

Commit

Permalink
Make repos public in public organizations
Browse files Browse the repository at this point in the history
* OSBS-7058

Signed-off-by: Martin Bašti <mbasti@redhat.com>
  • Loading branch information
MartinBasti committed Mar 13, 2019
1 parent 7037c3b commit b2bc591
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 21 deletions.
27 changes: 27 additions & 0 deletions README.md
Expand Up @@ -26,8 +26,35 @@ class ProdConfig:
# configuration of Koji URLs
KOJIHUB_URL = 'https://koji.fedoraproject.org/kojihub'
KOJIROOT_URL = 'https://kojipkgs.fedoraproject.org/'
# Organization access
organizations = {
"public-org": {
"public": True,
"oauth_token" "application_access_token_goes_here"
}
}
```

### Configuration of quay's organizations

#### Auto publishing new repositories

By default OMPS uses auth tokens for quay's CNR endpoint passed by user in HTTP
`Authorization` header (see Authorization section).

However CNR endpoint doesn't provide full access to quay applications.
OMPS needs oauth [access token](https://docs.quay.io/api/) to be able make
repositories public in chosen organizations.

Required permissions:
* Administer Repositories

Organizations configuration options:
* `public`: if `True` OMPS publish all new repositories in that organization
(requires `oauth_token`). Default is `False` repositories are private.
* `oauth_token`: application oauth access token from quay.io

## Running service

The best way is to run service from a container:
Expand Down
6 changes: 3 additions & 3 deletions omps/api/v1/packages.py
Expand Up @@ -9,7 +9,7 @@

from . import API
from omps.api.common import extract_auth_token
from omps.quay import QuayOrganization
from omps.quay import ORG_MANAGER

logger = logging.getLogger(__name__)

Expand All @@ -26,8 +26,8 @@ def delete_package_release(organization, repo, version=None):
:param version: version of operator manifest
:return: HTTP response
"""
token = extract_auth_token(request)
quay_org = QuayOrganization(organization, token)
cnr_token = extract_auth_token(request)
quay_org = ORG_MANAGER.get_org(organization, cnr_token)

# quay.io may contain OMPS incompatible release version format string
# but we want to be able to delete everything there, thus using _raw
Expand Down
6 changes: 3 additions & 3 deletions omps/api/v1/push.py
Expand Up @@ -24,7 +24,7 @@
QuayPackageNotFound,
)
from omps.koji_util import KOJI
from omps.quay import QuayOrganization, ReleaseVersion
from omps.quay import ReleaseVersion, ORG_MANAGER

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -163,8 +163,8 @@ def _zip_flow(*, organization, repo, version, extract_manifest_func,
:param extras_data: extra data added to response
:return: JSON response
"""
token = extract_auth_token(request)
quay_org = QuayOrganization(organization, token)
cnr_token = extract_auth_token(request)
quay_org = ORG_MANAGER.get_org(organization, cnr_token)

version = _get_package_version(quay_org, repo, version)
logger.info("Using release version: %s", version)
Expand Down
2 changes: 2 additions & 0 deletions omps/app.py
Expand Up @@ -9,6 +9,7 @@

from .api.v1 import API as API_V1
from .errors import init_errors_handling
from .quay import ORG_MANAGER
from .koji_util import KOJI
from .logger import init_logging
from .settings import init_config
Expand All @@ -33,6 +34,7 @@ def _load_config(app):
init_logging(conf)
logger.debug('Config loaded. Logging initialized')
KOJI.initialize(conf)
ORG_MANAGER.initialize(conf)


def _init_errors_handling(app):
Expand Down
143 changes: 136 additions & 7 deletions omps/quay.py
Expand Up @@ -9,6 +9,7 @@
from functools import total_ordering
import logging

from jsonschema import validate
import requests
from operatorcourier import api as courier_api

Expand All @@ -21,6 +22,20 @@
logger = logging.getLogger(__name__)


def get_error_msg(res):
"""Returns error message from quay's response
:param res: response
:rtype: str
:return: error message
"""
try:
msg = res.json()['error']['message']
except Exception:
msg = "Unknown error"
return msg


@total_ordering
class ReleaseVersion:
"""Quay package version"""
Expand Down Expand Up @@ -103,19 +118,94 @@ def increment(self):
self._z = 0


class OrgManager:

SCHEMA_ORGANIZATIONS = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Configuration for accessing Quay.io organizations",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$": {
"description": "Organization name",
"type": "object",
"properties": {
"public": {
"description": "True if organization is public",
"type": "boolean",
},
"oauth_token": {
"description": "quay.io application oauth access token",
"type": "string",
},
},
},
},
"uniqueItems": True,
"additionalProperties": False,
}

@classmethod
def validate_conf(cls, organizations):
"""Validate if config meets the schema expectations
:param organizations: organizations config
:raises jsonschema.ValidationError: when config doesn't meet criteria
"""
validate(organizations, cls.SCHEMA_ORGANIZATIONS)

def __init__(self):
self._organizations = None

def initialize(self, config):
self.validate_conf(config.organizations)
self._organizations = config.organizations

def get_org(self, organization, cnr_token):
org_config = self._organizations.get(organization, {})
return QuayOrganization(
organization,
cnr_token,
oauth_token=org_config.get('oauth_token'),
public=org_config.get('public', False)
)


class QuayOrganization:
"""Class for operations on organization"""

def __init__(self, organization, token):
def __init__(
self, organization, cnr_token, oauth_token=None, public=False
):
"""
:param organization: organization name
:param token: organization login token
:param cnr_token: organization login token (cnr endpoint)
:param oauth_token: oauth_access_token
:param public: organization is public
"""
self._quay_url = "https://quay.io"
self._organization = organization
self._token = token
self._token = cnr_token
self._oauth_token = oauth_token
self._public = public

@property
def public(self):
return self._public

@property
def oauth_access(self):
return bool(self._oauth_token)

def push_operator_manifest(self, repo, version, source_dir):
"""Build, verify and push operators artifact to quay.io registry
If organization is "public=True" this method ensures that repo will be
published.
:param repo: name of repository
:param version: release version
:param source_dir: path to directory with manifests
"""
try:
courier_api.build_verify_and_push(
self._organization, repo, version, self._token,
Expand All @@ -126,6 +216,19 @@ def push_operator_manifest(self, repo, version, source_dir):
"push_operator_manifest: Operator courier call failed: %s", e
)
raise QuayCourierError("Failed to push manifest: {}".format(e))
else:
if not self.public:
logger.debug(
"Organization '%s' is private, skipping publishing",
self._organization)
return
if not self.oauth_access:
logger.error(
"Cannot publish repository %s, Oauth access is not "
"configured for organization %s",
repo, self._organization)
return
self.publish_repo(repo)

def _get_repo_content(self, repo):
"""Return content of repository"""
Expand Down Expand Up @@ -246,14 +349,40 @@ def delete_release(self, repo, version):

if r.status_code != requests.codes.ok:

try:
msg = r.json()['error']['message']
except Exception:
msg = "Unknown error"
msg = get_error_msg(r)

if r.status_code == requests.codes.not_found:
logger.info("Delete release (404): %s", msg)
raise QuayPackageNotFound(msg)

logger.error("Delete release (%s): %s", r.status_code, msg)
raise QuayPackageError(msg)

def publish_repo(self, repo):
"""Make repository public
Needs OAUTH access
:param str repo: repository name
"""
assert self.oauth_access, "Needs Oauth access"
endpoint = '/api/v1/repository/{org}/{repo}/changevisibility'.format(
org=self._organization,
repo=repo,
)
url = '{q}{e}'.format(q=self._quay_url, e=endpoint)
data = {
"visibility": "public",
}
headers = {
"Authorization": "Bearer {}".format(self._oauth_token)
}
logger.debug("Publishing repository %s", repo)
r = requests.post(url, headers=headers, json=data)
if r.status_code != requests.codes.ok:
msg = get_error_msg(r)
logger.error("Publishing repository: %s", msg)
raise QuayPackageError(msg)


ORG_MANAGER = OrgManager()
16 changes: 16 additions & 0 deletions omps/settings.py
Expand Up @@ -7,7 +7,10 @@
import os
import sys

from jsonschema.exceptions import ValidationError

from . import constants
from .quay import OrgManager


class DefaultConfig:
Expand All @@ -17,6 +20,7 @@ class DefaultConfig:
DEBUG = False
TESTING = False
MAX_CONTENT_LENGTH = constants.DEFAULT_MAX_CONTENT_LENGTH
ORGANIZATIONS = {}


class ProdConfig(DefaultConfig):
Expand Down Expand Up @@ -112,6 +116,11 @@ class Config(object):
'default': "https://kojipkgs.fedoraproject.org/",
'desc': 'URL to koji root where build artifacts are stored'
},
'organizations': {
'type': dict,
'default': {},
'desc': 'Configuration of organizations'
}
}

def __init__(self, conf_section_obj):
Expand Down Expand Up @@ -220,3 +229,10 @@ def _setifok_default_release_version(self, s):
raise ValueError(
"default_release_version must be in format 'x.y.z'")
self._default_release_version = s

def _setifok_organizations(self, s):
try:
OrgManager.validate_conf(s)
except ValidationError as e:
raise ValueError("Organizations config: {}".format(e))
self._organizations = s
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -36,6 +36,7 @@
python_requires='>=3.6, <4',
install_requires=[
'Flask==1.0.*',
'jsonschema',
'koji',
'requests',
'operator-courier',
Expand Down

0 comments on commit b2bc591

Please sign in to comment.