Skip to content

Commit

Permalink
Add checking available resources (#114)
Browse files Browse the repository at this point in the history
* Added checking the availability of resources inside kubernetes

* Fix problem with newlines at end of files

* Fix problem with imports packages and other flake8 errors

* Fixed problems described in comments from PR

* Fixed issues described in PR
  • Loading branch information
Andrey Kazakov committed Jan 31, 2020
1 parent 97e3a42 commit 678304c
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 3 deletions.
11 changes: 8 additions & 3 deletions k8s_handle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
from k8s_handle import config
from k8s_handle import settings
from k8s_handle import templating
from k8s_handle.exceptions import DeprecationError, ProvisioningError
from k8s_handle.exceptions import DeprecationError, ProvisioningError, ResourceNotAvailableError
from k8s_handle.filesystem import InvalidYamlError
from k8s_handle.k8s.deprecation_checker import ApiDeprecationChecker
from k8s_handle.k8s.provisioner import Provisioner
from k8s_handle.k8s.availability_checker import ResourceAvailabilityChecker, make_resource_getters_list

COMMAND_DEPLOY = 'deploy'
COMMAND_DESTROY = 'destroy'
Expand Down Expand Up @@ -106,9 +107,11 @@ def _handler_provision(command, resources, priority_evaluator, use_kubeconfig, s

try:
deprecation_checker = ApiDeprecationChecker(client.VersionApi().get_code().git_version[1:])
available_checker = ResourceAvailabilityChecker(make_resource_getters_list())

for resource in resources:
deprecation_checker.run(resource)
available_checker.run(resource)
except client.api_client.ApiException:
log.warning("Error while getting API version, deprecation check will be skipped.")

Expand Down Expand Up @@ -228,11 +231,13 @@ def main():
log.error('{}'.format(e))
sys.exit(1)
except DeprecationError as e:
log.error('Deprecation warning: {}'.format(e))
sys.exit(1)
log.warning('Deprecation warning: {}'.format(e))
except RuntimeError as e:
log.error('RuntimeError: {}'.format(e))
sys.exit(1)
except ResourceNotAvailableError as e:
log.error('Resource not available: {}'.format(e))
sys.exit(1)
except ProvisioningError:
sys.exit(1)

Expand Down
4 changes: 4 additions & 0 deletions k8s_handle/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class DeprecationError(Exception):
pass


class ResourceNotAvailableError(Exception):
pass


class InvalidYamlError(Exception):
pass

Expand Down
29 changes: 29 additions & 0 deletions k8s_handle/k8s/api_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,32 @@ def list_api_resource_arbitrary(self, group, version):

log.error('{}'.format(add_indent(e.body)))
raise ProvisioningError(e)


class CoreResourcesAPI(client.CoreApi):

def list_api_resources(self, version):
try:
return self.api_client.call_api(
resource_path='/api/{}'.format(version),
method='GET',
header_params={
'Accept': self.api_client.select_header_accept(
['application/json', 'application/yaml', 'application/vnd.kubernetes.protobuf']
),
'Content-Type': self.api_client.select_header_content_type(
['application/json', 'application/yaml', 'application/vnd.kubernetes.protobuf']
)
},
response_type='V1APIResourceList',
auth_settings=['BearerToken'],
_return_http_data_only=True,
_preload_content=True,
)
except ApiException as e:
if e.reason == 'Not Found':
log.error('The resource definition with the specified version was not found')
return None

log.error('{}'.format(add_indent(e.body)))
raise ProvisioningError(e)
4 changes: 4 additions & 0 deletions k8s_handle/k8s/availability_checker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .checker import ResourceAvailabilityChecker
from .resource_getters import make_resource_getters_list

__all__ = ['ResourceAvailabilityChecker', 'make_resource_getters_list']
30 changes: 30 additions & 0 deletions k8s_handle/k8s/availability_checker/checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import List

from k8s_handle.templating import get_template_contexts
from k8s_handle.exceptions import ResourceNotAvailableError

from .resource_getters import AbstractResourceGetter


class ResourceAvailabilityChecker(object):

def __init__(self, resources_getters: List[AbstractResourceGetter]):
self.resources = resources_getters
self.versions = {}

def _is_available_kind(self, api_group: str, kind: str) -> bool:
kinds = []
for api in self.resources:
if api.is_processable_version(api_group):
kinds = api.get_resources_by_version(api_group)

return kind in kinds

def run(self, file_path: str):
for template_body in get_template_contexts(file_path):
if not self._is_available_kind(template_body.get('apiVersion'), template_body.get('kind')):
raise ResourceNotAvailableError(
"The resource with kind {} is not supported with version {}. File: {}".format(
template_body.get('kind'), template_body.get('apiVersion'), file_path
)
)
3 changes: 3 additions & 0 deletions k8s_handle/k8s/availability_checker/mocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MockResource(object):
def __init__(self, kind):
self.kind = kind
71 changes: 71 additions & 0 deletions k8s_handle/k8s/availability_checker/resource_getters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import re
from typing import List, Set

from k8s_handle.k8s.api_extensions import ResourcesAPI, CoreResourcesAPI

core_pattern = re.compile(r'^([^/]+)$')
regular_pattern = re.compile(r'^([^/]+)/([^/]+)$')


class AbstractResourceGetter(object):
def is_processable_version(self, api_group: str) -> bool:
raise NotImplementedError

def get_resources_by_version(self, api_group: str) -> Set[str]:
raise NotImplementedError


class CoreResourceGetter(AbstractResourceGetter):

def __init__(self, resource_api: CoreResourcesAPI):
self.api = resource_api
self.versions = {}

def is_processable_version(self, api_group: str) -> bool:
return core_pattern.match(api_group) is not None

def get_resources_by_version(self, api_group: str) -> Set[str]:
if api_group in self.versions:
return self.versions[api_group]

resp = self.api.list_api_resources(api_group)

if not resp:
return set()

kinds = {r.kind for r in resp.resources}
self.versions[api_group] = kinds

return kinds


class RegularResourceGetter(AbstractResourceGetter):

def __init__(self, resource_api: ResourcesAPI):
self.api = resource_api
self.versions = {}

def is_processable_version(self, api_group: str) -> bool:
return regular_pattern.match(api_group) is not None

def get_resources_by_version(self, api_group: str) -> Set[str]:
if api_group in self.versions:
return self.versions[api_group]

group, version = regular_pattern.findall(api_group).pop()
resp = self.api.list_api_resource_arbitrary(group, version)

if not resp:
return set()

kinds = {r.kind for r in resp.resources}
self.versions[api_group] = kinds

return kinds


def make_resource_getters_list() -> List[AbstractResourceGetter]:
return [
CoreResourceGetter(CoreResourcesAPI()),
RegularResourceGetter(ResourcesAPI())
]
39 changes: 39 additions & 0 deletions k8s_handle/k8s/availability_checker/test_availability_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from unittest import TestCase

from .checker import ResourceAvailabilityChecker
from .resource_getters import CoreResourceGetter, RegularResourceGetter
from .mocks import MockResource

from k8s_handle.k8s.mocks import ResourcesAPIMock
from k8s_handle.exceptions import ResourceNotAvailableError


class TestAvailabilityChecker(TestCase):

def setUp(self):
core_getter = CoreResourceGetter(
ResourcesAPIMock(group_version="v1", resources=[MockResource("Pod"), MockResource("CronJob")])
)

regular_getter = RegularResourceGetter(
ResourcesAPIMock(group_version="app/v1", resources=[MockResource("Deployment"), MockResource("Service")])
)

self.checker = ResourceAvailabilityChecker([core_getter, regular_getter])

def test_is_available_kind(self):
self.assertTrue(self.checker._is_available_kind("v1", "Pod"))
self.assertTrue(self.checker._is_available_kind("app/v1", "Service"))
self.assertFalse(self.checker._is_available_kind("v1", "Deployment"))
self.assertFalse(self.checker._is_available_kind("app/v1", "CronJob"))

def test_run_with_valid_version(self):
self.checker.run('k8s_handle/k8s/fixtures/valid_version.yaml')

def test_run_with_invalid_version(self):
with self.assertRaises(ResourceNotAvailableError):
self.checker.run('k8s_handle/k8s/fixtures/invalid_version.yaml')

def test_run_with_unsupported_version(self):
with self.assertRaises(ResourceNotAvailableError):
self.checker.run('k8s_handle/k8s/fixtures/unsupported_version.yaml')
43 changes: 43 additions & 0 deletions k8s_handle/k8s/availability_checker/test_resource_getters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from unittest import TestCase

from k8s_handle.k8s.mocks import ResourcesAPIMock

from .resource_getters import CoreResourceGetter, RegularResourceGetter
from .mocks import MockResource


class TestCoreResourceGetter(TestCase):

def setUp(self):
self.getter = CoreResourceGetter(
ResourcesAPIMock(group_version="v1", resources=[MockResource("Pod"), MockResource("CronJob")])
)

def test_is_processable_version(self):
self.assertTrue(self.getter.is_processable_version("v1"))
self.assertFalse(self.getter.is_processable_version("app/v1"))
self.assertFalse(self.getter.is_processable_version("/"))
self.assertFalse(self.getter.is_processable_version(""))

def test_get_resources_by_version(self):
self.assertSetEqual({"Pod", "CronJob"}, self.getter.get_resources_by_version("v1"))
self.assertSetEqual(set(), self.getter.get_resources_by_version("v2"))


class TestRegularResourceGetter(TestCase):

def setUp(self):
self.getter = RegularResourceGetter(
ResourcesAPIMock(group_version="app/v1", resources=[MockResource("Pod"), MockResource("CronJob")])
)

def test_is_processable_version(self):
self.assertFalse(self.getter.is_processable_version("v1"))
self.assertTrue(self.getter.is_processable_version("app/betav1"))
self.assertTrue(self.getter.is_processable_version("app/v1"))
self.assertFalse(self.getter.is_processable_version("/"))
self.assertFalse(self.getter.is_processable_version(""))

def test_get_resources_by_version(self):
self.assertSetEqual({"Pod", "CronJob"}, self.getter.get_resources_by_version("app/v1"))
self.assertSetEqual(set(), self.getter.get_resources_by_version("app/betav1"))
14 changes: 14 additions & 0 deletions k8s_handle/k8s/fixtures/invalid_version.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
apiVersion: extra/v1
kind: Service
metadata:
name: my-service
spec:
ports:
- name: HTTP
port: 80
protocol: TCP
targetPort: 80
selector:
app: my-app
type: ClusterIP
5 changes: 5 additions & 0 deletions k8s_handle/k8s/fixtures/unsupported_version.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Deployment
metadata:
name: my-service
14 changes: 14 additions & 0 deletions k8s_handle/k8s/fixtures/valid_version.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
apiVersion: app/v1
kind: Service
metadata:
name: my-service
spec:
ports:
- name: HTTP
port: 80
protocol: TCP
targetPort: 80
selector:
app: my-app
type: ClusterIP
6 changes: 6 additions & 0 deletions k8s_handle/k8s/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,9 @@ def list_api_resource_arbitrary(self, group, version):
return None

return V1APIResourceList(self._api_version, self._group_version, self._kind, self._resources)

def list_api_resources(self, version):
if not self._resources or self._group_version != version:
return None

return V1APIResourceList(self._api_version, self._group_version, self._kind, self._resources)

0 comments on commit 678304c

Please sign in to comment.