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

Commit

Permalink
Merge 7759cd3 into 7fb6757
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinBasti committed Apr 15, 2019
2 parents 7fb6757 + 7759cd3 commit 89e2a55
Show file tree
Hide file tree
Showing 16 changed files with 666 additions and 7 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ class ProdConfig:
"oauth_token" "application_access_token_goes_here"
}
}
# Greenwave integration
GREENWAVE = {
"url": "https://greenwave.example.com",
"context": "omps_push",
"product_version": "cvp"
}
```

### Configuration of quay's organizations
Expand All @@ -58,6 +65,17 @@ Organizations configuration options:
(requires `oauth_token`). Default is `False` repositories are private.
* `oauth_token`: application oauth access token from quay.io

### Greenwave integration

This is optional. When `GREENWAVE` settings are missing in config file checks
are skipped.

[Greenwave](https://pagure.io/greenwave) integration allows OMPS to check if
koji builds meets policies defined in Greenwave before operators from koji
builds are pushed to quay.
(Note: this check is skipped for pushing from zipfiles directly)


## Running service

The best way is to run service from a container:
Expand Down
13 changes: 12 additions & 1 deletion docs/usage/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,31 @@ Error messages have following format:
"error": "<error ID string>",
"message": "<detailed error description>",
}
```
For the following errors, the response will contain extra fields:
PackageValidationError:
"validation_info": <json with validation warnings and errors, empty if unavailable>
QuayAuthorizationError, QuayCourierError:
"quay_response": <json response from quay.io, empty if unavailable>
GreenwaveUnsatisfiedError, GreenwaveError:
"greenwave_response": <json with details from Greenwave (may be empty object)>
```

| HTTP Code / `status` | `error` | Explanation |
|-----------|------------------------|---------------------|
|400| OMPSUploadedFileError | Uploaded file didn't meet expectations (not a zip file, too big after unzip, corrupted zip file) |
|400| OMPSInvalidVersionFormat | Invalid version format in URL |
|400| KojiNotAnOperatorImage | Requested build is not an operator image |
|400| GreenwaveUnsatisfiedError | Build doesn't satisfy Greenwave policies |
|403| OMPSAuthorizationHeaderRequired| No `Authorization` header found in request|
|404| KojiNVRBuildNotFound | Requested build not found in koji |
|500| KojiManifestsArchiveNotFound | Manifest archive not found in koji build |
|500| KojiError | Koji generic error (connection failures, etc.) |
|500| QuayCourierError | operator-courier module raised exception during building and pushing manifests to quay|
|500| QuayPackageError | Failed to get information about application packages from quay |
|500| GreenwaveError | Failed to get information about koji build from Greenwave |


#### Example
Expand Down
12 changes: 12 additions & 0 deletions docs/usage/v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ Error messages have following format:
"error": "<error ID string>",
"message": "<detailed error description>",
}
For the following errors, the response will contain extra fields:
PackageValidationError:
"validation_info": <json with validation warnings and errors, empty if unavailable>
QuayAuthorizationError, QuayCourierError:
"quay_response": <json response from quay.io, empty if unavailable>
GreenwaveUnsatisfiedError, GreenwaveError:
"greenwave_response": <json with details from Greenwave (may be empty object)>
```


Expand All @@ -138,12 +148,14 @@ Error messages have following format:
|400| OMPSUploadedFileError | Uploaded file didn't meet expectations (not a zip file, too big after unzip, corrupted zip file) |
|400| OMPSInvalidVersionFormat | Invalid version format in URL |
|400| KojiNotAnOperatorImage | Requested build is not an operator image |
|400| GreenwaveUnsatisfiedError | Build doesn't satisfy Greenwave policies |
|403| OMPSAuthorizationHeaderRequired| No `Authorization` header found in request|
|404| KojiNVRBuildNotFound | Requested build not found in koji |
|500| KojiManifestsArchiveNotFound | Manifest archive not found in koji build |
|500| KojiError | Koji generic error (connection failures, etc.) |
|500| QuayCourierError | operator-courier module raised exception during building and pushing manifests to quay|
|500| QuayPackageError | Failed to get information about application packages from quay |
|500| GreenwaveError | Failed to get information about koji build from Greenwave |


#### Example
Expand Down
18 changes: 16 additions & 2 deletions omps/api/v1/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from requests.exceptions import RequestException

from . import API
from omps.errors import KojiError
from omps.errors import KojiError, GreenwaveError
from omps.greenwave import GREENWAVE
from omps.quay import get_cnr_api_version
from omps.koji_util import KOJI

Expand Down Expand Up @@ -60,6 +61,18 @@ def _err(exc):
else:
koji_result = _ok()

# greenwave status, Greenwave is optional integration
greenwave_result = None
if GREENWAVE.enabled:
try:
GREENWAVE.get_version()
except GreenwaveError as e:
logger.error('Greenwave version check: %s', e)
greenwave_result = _err(e)
everything_ok = False
else:
greenwave_result = _ok()

status_code = (
requests.codes.ok if everything_ok
else requests.codes.unavailable
Expand All @@ -70,7 +83,8 @@ def _err(exc):
"status": status_code,
"services": {
"koji": koji_result,
"quay": quay_result
"quay": quay_result,
"greenwave": greenwave_result,
}
}

Expand Down
3 changes: 3 additions & 0 deletions omps/api/v1/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
QuayPackageNotFound,
QuayCourierError,
)
from omps.greenwave import GREENWAVE
from omps.koji_util import KOJI
from omps.manifests_util import ManifestBundle
from omps.quay import ReleaseVersion, ORG_MANAGER
Expand Down Expand Up @@ -133,6 +134,8 @@ def extract_zip_file_from_koji(
after uncompressing
"""
with NamedTemporaryFile('wb', suffix='.zip') as tmpf:
if GREENWAVE.enabled:
GREENWAVE.check_build(nvr)
KOJI.download_manifest_archive(nvr, tmpf)
_extract_zip_file(tmpf.name, target_dir,
max_uncompressed_size=max_uncompressed_size)
Expand Down
2 changes: 2 additions & 0 deletions omps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .api.v1 import API as API_V1
from .api.v2 import API as API_V2
from .errors import init_errors_handling
from .greenwave import GREENWAVE
from .quay import ORG_MANAGER
from .koji_util import KOJI
from .logger import init_logging
Expand All @@ -36,6 +37,7 @@ def _load_config(app):
logger.debug('Config loaded. Logging initialized')
KOJI.initialize(conf)
ORG_MANAGER.initialize(conf)
GREENWAVE.initialize(conf)


def _init_errors_handling(app):
Expand Down
36 changes: 36 additions & 0 deletions omps/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,42 @@ class KojiError(OMPSError):
code = 500


class GreenwaveError(OMPSError):
"""Failed to retrieve data from Greenwave"""
code = 500

def __init__(self, msg, greenwave_response):
"""
:param msg: the message this exception should have
:param greenwave_response: response from greenwave
"""
super().__init__(msg)
self.greenwave_response = greenwave_response

def to_dict(self):
data = super().to_dict()
data['greenwave_response'] = self.greenwave_response
return data


class GreenwaveUnsatisfiedError(OMPSError):
"""Didn't meet policy expectations"""
code = 400

def __init__(self, msg, greenwave_response):
"""
:param msg: the message this exception should have
:param greenwave_response: response from greenwave
"""
super().__init__(msg)
self.greenwave_response = greenwave_response

def to_dict(self):
data = super().to_dict()
data['greenwave_response'] = self.greenwave_response
return data


def raise_for_courier_exception(e, new_msg=None):
"""React to operator-courier errors by raising the proper OMPS error
Expand Down
147 changes: 147 additions & 0 deletions omps/greenwave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

import logging

from jsonschema import validate
import requests

from omps.errors import (
GreenwaveError,
GreenwaveUnsatisfiedError
)

logger = logging.getLogger(__name__)


class GreenwaveHelper:
"""Greenwave helper"""

SCHEMA_CONFIG = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Configuration for Greenwave queries",
"type": "object",
"properties": {
"url": {
"description": "URL of Greenwave instance",
"type": "string",
},
"context": {
"description": "Greenwave decsion_context",
"type": "string",
},
"product_version": {
"description": "Greenwave product_version",
"type": "string",
},
},
"required": ["url", "context", "product_version"]
}

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

def __init__(self):
self._url = None
self._context = None
self._product_version = None
self._timeout = None

def initialize(self, conf):
if conf.greenwave is None:
logger.warning("Greenwave is not configured!")
return

self.validate_conf(conf.greenwave)
self._timeout = conf.request_timeout
self._url = conf.greenwave["url"]
self._context = conf.greenwave["context"]
self._product_version = conf.greenwave["product_version"]
if not self._url.endswith('/'):
self._url += "/"

@property
def enabled(self):
return self._url is not None

def check_build(self, nvr):
"""Check if build passed greenwave checks
:param str nvr: Build nvr to be checked
"""
endpoint = "api/v1.0/decision"

logger.info("'check_build' for %s", nvr)
if not self.enabled:
raise RuntimeError("Greenwave is not configured")

payload = dict(
decision_context=self._context,
product_version=self._product_version,
subject_identifier=nvr,
subject_type='koji_build',
)
try:
response = requests.post(
f"{self._url}{endpoint}", json=payload, timeout=self._timeout)
except requests.exceptions.RequestException as e:
logger.exception("Greenwave request failed")
raise GreenwaveError(f"Request failed: {e}", {})
else:
try:
data = response.json()
except ValueError:
data = {}

logger.debug("Greenwave details for %s: %s", nvr, data)
if response.status_code != requests.codes.ok:
raise GreenwaveError(
f"Greenwave unexpected response code: "
f"{response.status_code}", data)

try:
satisfied = data["policies_satisfied"]
except KeyError:
raise GreenwaveError(
f"Missing key 'policies_satisfied' in answer for nvr"
f" {nvr}", data)

if not satisfied:
raise GreenwaveUnsatisfiedError(
f"Policies for nvr {nvr} were not satisfied", data)

logger.info("check_build for %s: passed", nvr)

def get_version(self):
"""Get greenwave version
:raises GreenwaveError: when failed to get version
:rtype: str
:return: Greenwave version
"""
endpoint = 'api/v1.0/about'
if not self.enabled:
raise RuntimeError("Greenwave is not configured")

try:
response = requests.get(
f"{self._url}{endpoint}", timeout=self._timeout)
response.raise_for_status()
version = response.json()['version']
except (
requests.exceptions.RequestException, ValueError, KeyError
) as e:
raise GreenwaveError(f"Failed to get Greenwave version: {e}", {})

return version


GREENWAVE = GreenwaveHelper()
18 changes: 18 additions & 0 deletions omps/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from jsonschema.exceptions import ValidationError

from . import constants
from .greenwave import GreenwaveHelper
from .quay import OrgManager


Expand Down Expand Up @@ -127,6 +128,11 @@ class Config(object):
'default': None,
'desc': 'Timeout in seconds for Koji and Quay requests'
},
'greenwave': {
'type': dict,
'default': None,
'desc': 'Greenwave configuration'
},
}

def __init__(self, conf_section_obj):
Expand Down Expand Up @@ -242,3 +248,15 @@ def _setifok_organizations(self, s):
except ValidationError as e:
raise ValueError("Organizations config: {}".format(e))
self._organizations = s

def _setifok_greenwave(self, s):
if s is None:
# greenwave disabled
self._greenwave = s
return

try:
GreenwaveHelper.validate_conf(s)
except ValidationError as e:
raise ValueError(f"Grenwave config: {e}")
self._greenwave = s
Loading

0 comments on commit 89e2a55

Please sign in to comment.