Repository: 2gis/k8s-handle

Issue:
Add custom resources support
After #85 will be done we will want to deploy our [custom resources](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#create-custom-objects) too.

Example ([link](https://docs.cert-manager.io/en/latest/tasks/issuing-certificates/index.html#creating-certificate-resources)):
```yaml
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: example-com
spec:
  secretName: example-com-tls
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  commonName: example.com
  dnsNames:
  - example.com
  - www.example.com
  issuerRef:
    name: ca-issuer
    # We can reference ClusterIssuers by changing the kind here.
    # The default value is Issuer (i.e. a locally namespaced Issuer)
    kind: Issuer
```

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=h1) Report
> Merging [#93](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=desc) into [master](https://codecov.io/gh/2gis/k8s-handle/commit/081dee8bde857ef20d287669ac35d16f9e879fc1?src=pr&el=desc) will **decrease** coverage by `1.63%`.
> The diff coverage is `75.51%`.

[![Impacted file tree graph](https://codecov.io/gh/2gis/k8s-handle/pull/93/graphs/tree.svg?width=650&token=tv8Ij8dA6k&height=150&src=pr)](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #93      +/-   ##
==========================================
- Coverage   85.34%   83.71%   -1.64%     
==========================================
  Files          12       15       +3     
  Lines        1208     1308     +100     
==========================================
+ Hits         1031     1095      +64     
- Misses        177      213      +36
```


| [Impacted Files](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [k8s\_handle/k8s/test\_adapter.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9rOHMvdGVzdF9hZGFwdGVyLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [k8s\_handle/\_\_init\_\_.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9fX2luaXRfXy5weQ==) | `46.66% <100%> (+0.39%)` | :arrow_up: |
| [k8s\_handle/templating.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS90ZW1wbGF0aW5nLnB5) | `91.52% <100%> (-0.08%)` | :arrow_down: |
| [k8s\_handle/exceptions.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9leGNlcHRpb25zLnB5) | `100% <100%> (ø)` | |
| [k8s\_handle/k8s/test\_provisioner.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9rOHMvdGVzdF9wcm92aXNpb25lci5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [k8s\_handle/transforms.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS90cmFuc2Zvcm1zLnB5) | `100% <100%> (ø)` | |
| [k8s\_handle/k8s/deprecation\_checker.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9rOHMvZGVwcmVjYXRpb25fY2hlY2tlci5weQ==) | `92.85% <100%> (-0.25%)` | :arrow_down: |
| [k8s\_handle/filesystem.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9maWxlc3lzdGVtLnB5) | `83.33% <100%> (-0.67%)` | :arrow_down: |
| [k8s\_handle/k8s/test\_deprecation\_checker.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9rOHMvdGVzdF9kZXByZWNhdGlvbl9jaGVja2VyLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [k8s\_handle/k8s/adapters.py](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree#diff-azhzX2hhbmRsZS9rOHMvYWRhcHRlcnMucHk=) | `60.38% <60.38%> (ø)` | |
| ... and [4 more](https://codecov.io/gh/2gis/k8s-handle/pull/93/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=footer). Last update [081dee8...98e2bd4](https://codecov.io/gh/2gis/k8s-handle/pull/93?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dekhtyarev: @furiousassault , я посмотрел MR. 

Но я не уверен, что смогу адекватно найти в нём какие-то ошибки или проблемы. Поэтому давай подождём ещё ревьюверов(особенно @rvadim  и @FreakyGranny )

Patch:
diff --git a/README.md b/README.md
index 7596e92..4cafe9d 100644
--- a/README.md
+++ b/README.md
@@ -586,4 +586,13 @@ $ k8s-handle delete -r service.yaml --use-kubeconfig
 2019-02-15 14:24:06 INFO:k8s_handle.k8s.resource:Using namespace "test"
 2019-02-15 14:24:06 INFO:k8s_handle.k8s.resource:Trying to delete Service "k8s-handle-example"
 2019-02-15 14:24:06 INFO:k8s_handle.k8s.resource:Service "k8s-handle-example" deleted
-```
\ No newline at end of file
+```
+
+### Custom resource definitions and custom resources
+Since version 0.5.5 k8s-handle supports Custom resource definition (CRD) and custom resource (CR) kinds.
+If your deployment involves use of such kinds, make sure that CRD was deployed before CR and check correctness of the CRD's scope.
+
+Currently, in the case of a custom resource deployment, k8s-handle takes into account only the namespace 
+specified in the `metadata.namespace` field of the spec, in order to distinguish whether CR should be deployed/removed via namespaced API or not, without additional markers or flags. 
+
+Default k8s_namespace (taken from CLI, env or context) **is not applied** to such resources. Therefore, in order to create namespaced CR, one should explicitly set `metadata.namespace` field of that resource's spec.
diff --git a/k8s_handle/__init__.py b/k8s_handle/__init__.py
index aed0a37..2eb47a4 100644
--- a/k8s_handle/__init__.py
+++ b/k8s_handle/__init__.py
@@ -11,9 +11,10 @@ from kubernetes.config import list_kube_config_contexts, load_kube_config
 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.filesystem import InvalidYamlError
-from k8s_handle.k8s.deprecation_checker import ApiDeprecationChecker, DeprecationError
-from k8s_handle.k8s.resource import Provisioner, ProvisioningError
+from k8s_handle.k8s.deprecation_checker import ApiDeprecationChecker
+from k8s_handle.k8s.provisioner import Provisioner
 
 COMMAND_DEPLOY = 'deploy'
 COMMAND_DESTROY = 'destroy'
diff --git a/k8s_handle/exceptions.py b/k8s_handle/exceptions.py
new file mode 100644
index 0000000..1ac1f1f
--- /dev/null
+++ b/k8s_handle/exceptions.py
@@ -0,0 +1,14 @@
+class ProvisioningError(Exception):
+    pass
+
+
+class DeprecationError(Exception):
+    pass
+
+
+class InvalidYamlError(Exception):
+    pass
+
+
+class TemplateRenderingError(Exception):
+    pass
diff --git a/k8s_handle/filesystem.py b/k8s_handle/filesystem.py
index 7993290..9a6a3d7 100644
--- a/k8s_handle/filesystem.py
+++ b/k8s_handle/filesystem.py
@@ -5,15 +5,13 @@ import tempfile
 
 import yaml
 
+from k8s_handle.exceptions import InvalidYamlError
+
 # furiousassault RE: it's not a good practice to log from utility function
 # maybe we should pass os.remove failure silently, it doesn't seem so important
 log = logging.getLogger(__name__)
 
 
-class InvalidYamlError(Exception):
-    pass
-
-
 def load_yaml(path):
     try:
         with open(path) as f:
diff --git a/k8s_handle/k8s/adapters.py b/k8s_handle/k8s/adapters.py
new file mode 100644
index 0000000..e8ec4ce
--- /dev/null
+++ b/k8s_handle/k8s/adapters.py
@@ -0,0 +1,295 @@
+import logging
+
+from kubernetes import client
+from kubernetes.client.rest import ApiException
+
+from k8s_handle import settings
+from k8s_handle.exceptions import ProvisioningError
+from k8s_handle.transforms import add_indent, split_str_by_capital_letters
+from .api_extensions import ResourcesAPI
+from .mocks import K8sClientMock
+
+log = logging.getLogger(__name__)
+
+
+class Adapter:
+    api_versions = {
+        'apps/v1beta1': client.AppsV1beta1Api,
+        'v1': client.CoreV1Api,
+        'extensions/v1beta1': client.ExtensionsV1beta1Api,
+        'batch/v1': client.BatchV1Api,
+        'batch/v2alpha1': client.BatchV2alpha1Api,
+        'batch/v1beta1': client.BatchV1beta1Api,
+        'policy/v1beta1': client.PolicyV1beta1Api,
+        'storage.k8s.io/v1': client.StorageV1Api,
+        'apps/v1': client.AppsV1Api,
+        'autoscaling/v1': client.AutoscalingV1Api,
+        'rbac.authorization.k8s.io/v1': client.RbacAuthorizationV1Api,
+        'scheduling.k8s.io/v1alpha1': client.SchedulingV1alpha1Api,
+        'scheduling.k8s.io/v1beta1': client.SchedulingV1beta1Api,
+        'networking.k8s.io/v1': client.NetworkingV1Api,
+        'apiextensions.k8s.io/v1beta1': client.ApiextensionsV1beta1Api,
+    }
+    kinds_builtin = [
+        'ConfigMap', 'CronJob', 'DaemonSet', 'Deployment', 'Endpoints',
+        'Ingress', 'Job', 'Namespace', 'PodDisruptionBudget', 'ResourceQuota',
+        'Secret', 'Service', 'ServiceAccount', 'StatefulSet', 'StorageClass',
+        'PersistentVolume', 'PersistentVolumeClaim', 'HorizontalPodAutoscaler',
+        'Role', 'RoleBinding', 'ClusterRole', 'ClusterRoleBinding', 'CustomResourceDefinition',
+        'PriorityClass', 'PodSecurityPolicy', 'LimitRange', 'NetworkPolicy'
+    ]
+
+    def __init__(self, spec):
+        self.body = spec
+        self.kind = spec.get('kind', "")
+        self.name = spec.get('metadata', {}).get('name')
+        self.namespace = spec.get('metadata', {}).get('namespace', "") or settings.K8S_NAMESPACE
+
+    @staticmethod
+    def get_instance(spec, api_custom_objects=None, api_resources=None):
+        # due to https://github.com/kubernetes-client/python/issues/387
+        if spec.get('kind') in Adapter.kinds_builtin:
+            if spec.get('apiVersion') == 'test/test':
+                return AdapterBuiltinKind(spec, K8sClientMock(spec.get('metadata', {}).get('name')))
+
+            api = Adapter.api_versions.get(spec.get('apiVersion'))
+
+            if not api:
+                return None
+
+            return AdapterBuiltinKind(spec, api())
+
+        api_custom_objects = api_custom_objects or client.CustomObjectsApi()
+        api_resources = api_resources or ResourcesAPI()
+        return AdapterCustomKind(spec, api_custom_objects, api_resources)
+
+
+class AdapterBuiltinKind(Adapter):
+    def __init__(self, spec, api=None):
+        super().__init__(spec)
+        self.kind = split_str_by_capital_letters(spec['kind'])
+        self.replicas = spec.get('spec', {}).get('replicas')
+        self.api = api
+
+    def get(self):
+        try:
+            if hasattr(self.api, "read_namespaced_{}".format(self.kind)):
+                response = getattr(self.api, 'read_namespaced_{}'.format(self.kind))(
+                    self.name, namespace=self.namespace)
+            else:
+                response = getattr(self.api, 'read_{}'.format(self.kind))(self.name)
+        except ApiException as e:
+            if e.reason == 'Not Found':
+                return None
+            log.error('Exception when calling "read_namespaced_{}": {}'.format(self.kind, add_indent(e.body)))
+            raise ProvisioningError(e)
+
+        return response
+
+    def get_pods_by_selector(self, label_selector):
+        try:
+            if not isinstance(self.api, K8sClientMock):
+                self.api = client.CoreV1Api()
+
+            return self.api.list_namespaced_pod(
+                namespace=self.namespace, label_selector='job-name={}'.format(label_selector))
+
+        except ApiException as e:
+            log.error('Exception when calling CoreV1Api->list_namespaced_pod: {}', e)
+            raise e
+
+    def read_pod_status(self, name):
+        try:
+            if not isinstance(self.api, K8sClientMock):
+                self.api = client.CoreV1Api()
+
+            return self.api.read_namespaced_pod_status(name, namespace=self.namespace)
+        except ApiException as e:
+            log.error('Exception when calling CoreV1Api->read_namespaced_pod_status: {}', e)
+            raise e
+
+    def read_pod_logs(self, name, container):
+        log.info('Read logs for pod "{}", container "{}"'.format(name, container))
+        try:
+            if not isinstance(self.api, K8sClientMock):
+                self.api = client.CoreV1Api()
+            if settings.COUNT_LOG_LINES:
+                return self.api.read_namespaced_pod_log(name, namespace=self.namespace, timestamps=True,
+                                                        tail_lines=settings.COUNT_LOG_LINES, container=container)
+            return self.api.read_namespaced_pod_log(name, namespace=self.namespace, timestamps=True,
+                                                    container=container)
+        except ApiException as e:
+            log.error('Exception when calling CoreV1Api->read_namespaced_pod_log: {}', e)
+            raise e
+
+    def create(self):
+        try:
+            if hasattr(self.api, "create_namespaced_{}".format(self.kind)):
+                return getattr(self.api, 'create_namespaced_{}'.format(self.kind))(
+                    body=self.body, namespace=self.namespace)
+
+            return getattr(self.api, 'create_{}'.format(self.kind))(body=self.body)
+        except ApiException as e:
+            log.error('Exception when calling "create_namespaced_{}": {}'.format(self.kind, add_indent(e.body)))
+            raise ProvisioningError(e)
+        except ValueError as e:
+            log.error(e)
+            # WORKAROUND https://github.com/kubernetes-client/python/issues/466
+            # also https://github.com/kubernetes-client/gen/issues/52
+            if self.kind not in ['pod_disruption_budget', 'custom_resource_definition']:
+                raise e
+
+    def replace(self, parameters):
+        try:
+            if self.kind in ['custom_resource_definition']:
+                self.body['metadata']['resourceVersion'] = parameters['resourceVersion']
+                return self.api.replace_custom_resource_definition(
+                    self.name, self.body,
+                )
+
+            if self.kind in ['service', 'service_account']:
+                if 'spec' in self.body:
+                    self.body['spec']['ports'] = parameters.get('ports')
+
+                return getattr(self.api, 'patch_namespaced_{}'.format(self.kind))(
+                    name=self.name, body=self.body, namespace=self.namespace
+                )
+
+            if hasattr(self.api, "replace_namespaced_{}".format(self.kind)):
+                return getattr(self.api, 'replace_namespaced_{}'.format(self.kind))(
+                    name=self.name, body=self.body, namespace=self.namespace)
+
+            return getattr(self.api, 'replace_{}'.format(self.kind))(
+                name=self.name, body=self.body)
+        except ApiException as e:
+            log.error('Exception when calling "replace_namespaced_{}": {}'.format(self.kind, add_indent(e.body)))
+            raise ProvisioningError(e)
+
+    def delete(self):
+        try:
+            if hasattr(self.api, "delete_namespaced_{}".format(self.kind)):
+                return getattr(self.api, 'delete_namespaced_{}'.format(self.kind))(
+                    name=self.name, body=client.V1DeleteOptions(propagation_policy='Foreground'),
+                    namespace=self.namespace)
+
+            return getattr(self.api, 'delete_{}'.format(self.kind))(
+                name=self.name, body=client.V1DeleteOptions(propagation_policy='Foreground'))
+        except ApiException as e:
+            if e.reason == 'Not Found':
+                return None
+            log.error('Exception when calling "delete_namespaced_{}": {}'.format(self.kind, add_indent(e.body)))
+            raise ProvisioningError(e)
+
+
+class AdapterCustomKind(Adapter):
+    def __init__(self, spec, api_custom_objects, api_resources):
+        super().__init__(spec)
+        self.api = api_custom_objects
+        self.api_resources = api_resources
+        self.plural = None
+
+        try:
+            api_version_splitted = spec.get('apiVersion').split('/', 1)
+            self.group = api_version_splitted[0]
+            self.version = api_version_splitted[1]
+        except (IndexError, AttributeError):
+            self.group = None
+            self.version = None
+
+        resources_list = self.api_resources.list_api_resource_arbitrary(self.group, self.version)
+
+        if not resources_list:
+            return
+
+        for resource in resources_list.resources:
+            if resource.kind != self.kind:
+                continue
+
+            self.plural = resource.name
+
+            if not resource.namespaced:
+                self.namespace = ""
+
+            break
+
+    def get(self):
+        self._validate()
+
+        try:
+            if self.namespace:
+                return self.api.get_namespaced_custom_object(
+                    self.group, self.version, self.namespace, self.plural, self.name
+                )
+
+            return self.api.get_cluster_custom_object(self.group, self.version, self.plural, self.name)
+
+        except ApiException as e:
+            if e.reason == 'Not Found':
+                return None
+
+            log.error('{}'.format(add_indent(e.body)))
+            raise ProvisioningError(e)
+
+    def create(self):
+        self._validate()
+
+        try:
+            if self.namespace:
+                return self.api.create_namespaced_custom_object(
+                    self.group, self.version, self.namespace, self.plural, self.body
+                )
+
+            return self.api.create_cluster_custom_object(self.group, self.version, self.plural, self.body)
+
+        except ApiException as e:
+            log.error('{}'.format(add_indent(e.body)))
+            raise ProvisioningError(e)
+
+    def delete(self):
+        self._validate()
+
+        try:
+            if self.namespace:
+                return self.api.delete_namespaced_custom_object(
+                    self.group, self.version, self.namespace, self.plural, self.name,
+                    client.V1DeleteOptions(propagation_policy='Foreground')
+                )
+
+            return self.api.delete_cluster_custom_object(
+                self.group, self.version, self.plural, self.name,
+                client.V1DeleteOptions(propagation_policy='Foreground')
+            )
+
+        except ApiException as e:
+            if e.reason == 'Not Found':
+                return None
+
+            log.error(
+                '{}'.format(add_indent(e.body)))
+            raise ProvisioningError(e)
+
+    def replace(self, _):
+        self._validate()
+
+        try:
+            if self.namespace:
+                return self.api.patch_namespaced_custom_object(
+                    self.group, self.version, self.namespace, self.plural, self.name, self.body
+                )
+
+            return self.api.patch_cluster_custom_object(
+                self.group, self.version, self.plural, self.name, self.body
+            )
+        except ApiException as e:
+            log.error('{}'.format(add_indent(e.body)))
+            raise ProvisioningError(e)
+
+    def _validate(self):
+        if not self.plural:
+            raise RuntimeError("No valid plural name of resource definition discovered")
+
+        if not self.group:
+            raise RuntimeError("No valid resource definition group discovered")
+
+        if not self.version:
+            raise RuntimeError("No valid version of resource definition supplied")
diff --git a/k8s_handle/k8s/api_extensions.py b/k8s_handle/k8s/api_extensions.py
new file mode 100644
index 0000000..e52b59b
--- /dev/null
+++ b/k8s_handle/k8s/api_extensions.py
@@ -0,0 +1,44 @@
+import logging
+
+from kubernetes import client
+from kubernetes.client.rest import ApiException
+
+from k8s_handle.exceptions import ProvisioningError
+from k8s_handle.transforms import add_indent
+
+log = logging.getLogger(__name__)
+
+
+class ResourcesAPI(client.ApisApi):
+    def list_api_resource_arbitrary(self, group, version):
+        try:
+            return self.api_client.call_api(
+                '/apis/{}/{}'.format(group, version), 'GET',
+                {},
+                [],
+                {
+                    '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']
+                    )
+                },
+                body=None,
+                post_params=[],
+                files={},
+                response_type='V1APIResourceList',
+                auth_settings=['BearerToken'],
+                async_req=None,
+                _return_http_data_only=True,
+                _preload_content=True,
+                _request_timeout=None,
+                collection_formats={}
+            )
+        except ApiException as e:
+            if e.reason == 'Not Found':
+                log.error('The resource definition with the specified group and version was not found')
+                return None
+
+            log.error('{}'.format(add_indent(e.body)))
+            raise ProvisioningError(e)
diff --git a/k8s_handle/k8s/deprecation_checker.py b/k8s_handle/k8s/deprecation_checker.py
index 6d44f57..adc2b07 100644
--- a/k8s_handle/k8s/deprecation_checker.py
+++ b/k8s_handle/k8s/deprecation_checker.py
@@ -3,14 +3,11 @@ import logging
 import semver
 
 from k8s_handle.templating import get_template_contexts
+from k8s_handle.exceptions import DeprecationError
 
 log = logging.getLogger(__name__)
 
 
-class DeprecationError(Exception):
-    pass
-
-
 class ApiDeprecationChecker:
     def __init__(self, server_version):
         self.server_version = server_version
diff --git a/k8s_handle/k8s/mocks.py b/k8s_handle/k8s/mocks.py
index 3efea1f..f57b13a 100644
--- a/k8s_handle/k8s/mocks.py
+++ b/k8s_handle/k8s/mocks.py
@@ -1,6 +1,8 @@
-from kubernetes.client.rest import ApiException
 from collections import namedtuple
 
+from kubernetes.client import V1APIResourceList
+from kubernetes.client.rest import ApiException
+
 
 class K8sClientMock:
     def __init__(self, name=None):
@@ -289,3 +291,21 @@ class ServicePort:
             self.target_port = port
         else:
             self.target_port = port
+
+
+class CustomObjectsAPIMock:
+    pass
+
+
+class ResourcesAPIMock:
+    def __init__(self, api_version=None, group_version=None, resources=None):
+        self._resources = resources
+        self._api_version = api_version
+        self._group_version = group_version
+        self._kind = 'APIResourceList'
+
+    def list_api_resource_arbitrary(self, group, version):
+        if not self._resources or self._group_version != '{}/{}'.format(group, version):
+            return None
+
+        return V1APIResourceList(self._api_version, self._group_version, self._kind, self._resources)
diff --git a/k8s_handle/k8s/resource.py b/k8s_handle/k8s/provisioner.py
similarity index 59%
rename from k8s_handle/k8s/resource.py
rename to k8s_handle/k8s/provisioner.py
index eda2ecd..a8f35dc 100644
--- a/k8s_handle/k8s/resource.py
+++ b/k8s_handle/k8s/provisioner.py
@@ -1,32 +1,18 @@
-import json
 import logging
-import re
 from time import sleep
 
-from kubernetes import client
 from kubernetes.client.models.v1_label_selector import V1LabelSelector
 from kubernetes.client.models.v1_label_selector_requirement import V1LabelSelectorRequirement
 from kubernetes.client.models.v1_resource_requirements import V1ResourceRequirements
-from kubernetes.client.rest import ApiException
 
 from k8s_handle import settings
 from k8s_handle.templating import get_template_contexts
-from .mocks import K8sClientMock
+from k8s_handle.transforms import split_str_by_capital_letters
+from .adapters import Adapter
 
 log = logging.getLogger(__name__)
 
 
-class ProvisioningError(Exception):
-    pass
-
-
-def _split_str_by_capital_letters(item):
-    # upper the first letter
-    item = item[0].upper() + item[1:]
-    # transform 'Service' to 'service', 'CronJob' to 'cron_job', 'TargetPort' to 'target_port', etc.
-    return '_'.join(re.findall(r'[A-Z][^A-Z]*', item)).lower()
-
-
 class Provisioner:
     def __init__(self, command, sync_mode, show_logs):
         self.command = command
@@ -41,7 +27,7 @@ class Provisioner:
     @staticmethod
     def _ports_are_equal(old_port, new_port):
         for new_key in new_port.keys():
-            old_key = _split_str_by_capital_letters(new_key)
+            old_key = split_str_by_capital_letters(new_key)
             if getattr(old_port, old_key, None) != new_port.get(new_key, None):
                 return False
         return True
@@ -156,7 +142,7 @@ class Provisioner:
 
     def _is_pvc_specs_equals(self, old_obj, new_dict):
         for new_key in new_dict.keys():
-            old_key = _split_str_by_capital_letters(new_key)
+            old_key = split_str_by_capital_letters(new_key)
 
             # if template has some new attributes
             if not hasattr(old_obj, old_key):
@@ -186,69 +172,74 @@ class Provisioner:
             self._deploy(template_body, file_path)
 
     def _deploy(self, template_body, file_path):
-        kube_client = Adapter(template_body)
-        log.info('Using namespace "{}"'.format(kube_client.namespace))
+        kube_client = Adapter.get_instance(template_body)
 
-        if kube_client.api is None:
-            raise RuntimeError('Unknown apiVersion "{}" in template "{}"'.format(template_body['apiVersion'],
-                                                                                 file_path))
+        if not kube_client:
+            raise RuntimeError(
+                'Unknown apiVersion "{}" in template "{}"'.format(
+                    template_body['apiVersion'],
+                    file_path
+                )
+            )
+
+        log.info('Using namespace "{}"'.format(kube_client.namespace))
+        resource = kube_client.get()
 
-        if kube_client.get() is None:
+        if resource is None:
             log.info('{} "{}" does not exist, create it'.format(template_body['kind'], kube_client.name))
             kube_client.create()
         else:
             log.info('{} "{}" already exists, replace it'.format(template_body['kind'], kube_client.name))
+            parameters = {}
 
-            apply_ports = None
             if template_body['kind'] == 'Service':
-                resource = kube_client.get()
-
                 missing_annotations, missing_labels = \
                     self._get_missing_annotations_and_labels(resource.metadata, template_body['metadata'])
-                apply_ports = self._get_apply_ports(resource.spec, template_body['spec'])
+                parameters['ports'] = self._get_apply_ports(resource.spec, template_body['spec'])
 
                 self._notify_about_missing_items_in_template(missing_annotations, 'annotation')
                 self._notify_about_missing_items_in_template(missing_labels, 'label')
 
-                if len(apply_ports) != 0:
-                    log.info('Next ports will be apply: {}'.format(apply_ports))
+                if parameters['ports']:
+                    log.info('Next ports will be applied: {}'.format(parameters['ports']))
 
             if template_body['kind'] == 'PersistentVolumeClaim':
-                resource = kube_client.get()
                 if self._is_pvc_specs_equals(resource.spec, template_body['spec']):
                     log.info('PersistentVolumeClaim is not changed')
                     return
 
             if template_body['kind'] == 'PersistentVolume':
-                resource = kube_client.get()
                 if resource.status.phase in ['Bound', 'Released']:
                     log.warning('PersistentVolume has "{}" status, skip replacing'.format(resource.status.phase))
                     return
 
-            kube_client.replace(apply_ports)
+            if template_body['kind'] == 'CustomResourceDefinition':
+                parameters['resourceVersion'] = resource.metadata.resource_version
 
-        if template_body['kind'] == 'Deployment' and self.sync_mode:
-            self._wait_deployment_complete(kube_client,
-                                           tries=settings.CHECK_STATUS_TRIES,
-                                           timeout=settings.CHECK_STATUS_TIMEOUT)
+            kube_client.replace(parameters)
 
-        if template_body['kind'] == 'StatefulSet' and self.sync_mode:
-            self._wait_statefulset_complete(kube_client,
-                                            tries=settings.CHECK_STATUS_TRIES,
-                                            timeout=settings.CHECK_STATUS_TIMEOUT)
+        if self.sync_mode:
+            if template_body['kind'] == 'Deployment':
+                self._wait_deployment_complete(kube_client,
+                                               tries=settings.CHECK_STATUS_TRIES,
+                                               timeout=settings.CHECK_STATUS_TIMEOUT)
+
+            if template_body['kind'] == 'StatefulSet':
+                self._wait_statefulset_complete(kube_client,
+                                                tries=settings.CHECK_STATUS_TRIES,
+                                                timeout=settings.CHECK_STATUS_TIMEOUT)
 
-        if template_body['kind'] == 'DaemonSet' and self.sync_mode:
-            self._wait_daemonset_complete(kube_client,
-                                          tries=settings.CHECK_STATUS_TRIES,
-                                          timeout=settings.CHECK_STATUS_TIMEOUT)
+            if template_body['kind'] == 'DaemonSet':
+                self._wait_daemonset_complete(kube_client,
+                                              tries=settings.CHECK_STATUS_TRIES,
+                                              timeout=settings.CHECK_STATUS_TIMEOUT)
 
-        if template_body['kind'] == 'Job' and self.sync_mode:
-            return self._wait_job_complete(kube_client,
-                                           tries=settings.CHECK_STATUS_TRIES,
-                                           timeout=settings.CHECK_STATUS_TIMEOUT)
+            if template_body['kind'] == 'Job':
+                return self._wait_job_complete(kube_client,
+                                               tries=settings.CHECK_STATUS_TRIES,
+                                               timeout=settings.CHECK_STATUS_TIMEOUT)
 
         if template_body['kind'] == 'Job' and self.show_logs:
-            log.info("Got into section")
             pod_name, pod_containers = self._get_pod_name_and_containers_by_selector(
                 kube_client,
                 template_body['metadata']['name'],
@@ -278,27 +269,38 @@ class Provisioner:
             self._destroy(template_body, file_path)
 
     def _destroy(self, template_body, file_path):
-        kube_client = Adapter(template_body)
+        kube_client = Adapter.get_instance(template_body)
+
+        if not kube_client:
+            raise RuntimeError(
+                'Unknown apiVersion "{}" in template "{}"'.format(
+                    template_body['apiVersion'],
+                    file_path
+                )
+            )
+
         log.info('Using namespace "{}"'.format(kube_client.namespace))
         log.info('Trying to delete {} "{}"'.format(template_body['kind'], kube_client.name))
-        if kube_client.api is None:
-            raise RuntimeError('Unknown apiVersion "{}" in template "{}"'.format(template_body['apiVersion'],
-                                                                                 file_path))
-
         response = kube_client.delete()
+
         if response is None:
+            log.info("{} {} is not found".format(template_body['kind'], kube_client.name))
             return
 
-        if response.message is not None:
+        # custom objects api response is a simple dictionary without message field
+        if hasattr(response, 'message') and response.message is not None:
             raise RuntimeError('{} "{}" deletion failed: {}'.format(
                 template_body['kind'], kube_client.name, response.message))
-        else:
-            if self.sync_mode:
-                self._wait_destruction_complete(kube_client, template_body['kind'],
-                                                tries=settings.CHECK_STATUS_TRIES,
-                                                timeout=settings.CHECK_STATUS_TIMEOUT)
 
-            log.info('{} "{}" deleted'.format(template_body['kind'], kube_client.name))
+        if isinstance(response, dict) and not response.get('metadata', {}).get('deletionTimestamp'):
+            raise RuntimeError('{} "{}" deletion failed: {}'.format(template_body['kind'], kube_client.name, response))
+
+        if self.sync_mode:
+            self._wait_destruction_complete(kube_client, template_body['kind'],
+                                            tries=settings.CHECK_STATUS_TRIES,
+                                            timeout=settings.CHECK_STATUS_TIMEOUT)
+
+        log.info('{} "{}" has been deleted'.format(template_body['kind'], kube_client.name))
 
     @staticmethod
     def _get_pod_name_and_containers_by_selector(kube_client, selector, tries, timeout):
@@ -415,164 +417,3 @@ class Provisioner:
                          'next attempt in {} sec.'.format(kind, i + 1, timeout))
 
         raise RuntimeError('{} destruction not completed for {} tries'.format(kind, tries))
-
-
-class Adapter:
-    client_version_mapping = {
-        'apps/v1beta1': client.AppsV1beta1Api,
-        'v1': client.CoreV1Api,
-        'extensions/v1beta1': client.ExtensionsV1beta1Api,
-        'batch/v1': client.BatchV1Api,
-        'batch/v2alpha1': client.BatchV2alpha1Api,
-        'batch/v1beta1': client.BatchV1beta1Api,
-        'policy/v1beta1': client.PolicyV1beta1Api,
-        'storage.k8s.io/v1': client.StorageV1Api,
-        'apps/v1': client.AppsV1Api,
-        'autoscaling/v1': client.AutoscalingV1Api,
-        'rbac.authorization.k8s.io/v1': client.RbacAuthorizationV1Api,
-        'scheduling.k8s.io/v1alpha1': client.SchedulingV1alpha1Api,
-        'scheduling.k8s.io/v1beta1': client.SchedulingV1beta1Api,
-        'networking.k8s.io/v1': client.NetworkingV1Api,
-        'apiextensions.k8s.io/v1beta1': client.ApiextensionsV1beta1Api,
-    }
-
-    def __init__(self, spec, api=None):
-        self.kind = self._get_app_kind(spec['kind'])
-        self.name = spec['metadata']['name']
-        self.body = spec
-        self.replicas = None if 'spec' not in spec or 'replicas' not in spec['spec'] else spec['spec']['replicas']
-        self.namespace = spec['metadata']['namespace'] if 'namespace' in spec['metadata'] else settings.K8S_NAMESPACE
-        self.api = api or self._detect_api_object(spec['apiVersion'])
-
-    def _detect_api_object(self, api_version):
-        # Due to https://github.com/kubernetes-client/python/issues/387
-        if api_version == 'test/test':
-            return K8sClientMock(self.name)
-
-        api = self.client_version_mapping.get(api_version)
-
-        if not api:
-            return None
-
-        return api()
-
-    @staticmethod
-    def _get_app_kind(kind):
-        if kind not in ['ConfigMap', 'CronJob', 'DaemonSet', 'Deployment', 'Endpoints',
-                        'Ingress', 'Job', 'Namespace', 'PodDisruptionBudget', 'ResourceQuota',
-                        'Secret', 'Service', 'ServiceAccount', 'StatefulSet', 'StorageClass',
-                        'PersistentVolume', 'PersistentVolumeClaim', 'HorizontalPodAutoscaler',
-                        'Role', 'RoleBinding', 'ClusterRole', 'ClusterRoleBinding', 'CustomResourceDefinition',
-                        'PriorityClass', 'PodSecurityPolicy', 'LimitRange', 'NetworkPolicy']:
-            raise RuntimeError('Unknown kind "{}" in generated file'.format(kind))
-
-        return _split_str_by_capital_letters(kind)
-
-    def get(self):
-        try:
-            if hasattr(self.api, "read_namespaced_{}".format(self.kind)):
-                response = getattr(self.api, 'read_namespaced_{}'.format(self.kind))(
-                    self.name, namespace=self.namespace)
-            else:
-                response = getattr(self.api, 'read_{}'.format(self.kind))(self.name)
-        except ApiException as e:
-            if e.reason == 'Not Found':
-                return None
-            log.error('Exception when calling "read_namespaced_{}": {}'.format(self.kind, self._add_indent(e.body)))
-            raise ProvisioningError(e)
-
-        return response
-
-    def get_pods_by_selector(self, label_selector):
-        try:
-            if not isinstance(self.api, K8sClientMock):
-                self.api = client.CoreV1Api()
-
-            return self.api.list_namespaced_pod(
-                namespace=self.namespace, label_selector='job-name={}'.format(label_selector))
-
-        except ApiException as e:
-            log.error('Exception when calling CoreV1Api->list_namespaced_pod: {}', e)
-            raise e
-
-    def read_pod_status(self, name):
-        try:
-            if not isinstance(self.api, K8sClientMock):
-                self.api = client.CoreV1Api()
-
-            return self.api.read_namespaced_pod_status(name, namespace=self.namespace)
-        except ApiException as e:
-            log.error('Exception when calling CoreV1Api->read_namespaced_pod_status: {}', e)
-            raise e
-
-    def read_pod_logs(self, name, container):
-        log.info('Read logs for pod "{}", container "{}"'.format(name, container))
-        try:
-            if not isinstance(self.api, K8sClientMock):
-                self.api = client.CoreV1Api()
-            if settings.COUNT_LOG_LINES:
-                return self.api.read_namespaced_pod_log(name, namespace=self.namespace, timestamps=True,
-                                                        tail_lines=settings.COUNT_LOG_LINES, container=container)
-            return self.api.read_namespaced_pod_log(name, namespace=self.namespace, timestamps=True,
-                                                    container=container)
-        except ApiException as e:
-            log.error('Exception when calling CoreV1Api->read_namespaced_pod_log: {}', e)
-            raise e
-
-    def create(self):
-        try:
-            if hasattr(self.api, "create_namespaced_{}".format(self.kind)):
-                return getattr(self.api, 'create_namespaced_{}'.format(self.kind))(
-                    body=self.body, namespace=self.namespace)
-
-            return getattr(self.api, 'create_{}'.format(self.kind))(body=self.body)
-        except ApiException as e:
-            log.error('Exception when calling "create_namespaced_{}": {}'.format(self.kind, self._add_indent(e.body)))
-            raise ProvisioningError(e)
-        except ValueError as e:
-            log.error(e)
-            # WORKAROUND https://github.com/kubernetes-client/python/issues/466
-            # also https://github.com/kubernetes-client/gen/issues/52
-            if self.kind not in ['pod_disruption_budget', 'custom_resource_definition']:
-                raise e
-
-    def replace(self, ports=None):
-        try:
-            if self.kind in ['service', 'service_account', ]:
-                if ports is not None:
-                    self.body['spec']['ports'] = ports
-                return getattr(self.api, 'patch_namespaced_{}'.format(self.kind))(
-                    name=self.name, body=self.body, namespace=self.namespace
-                )
-
-            if hasattr(self.api, "replace_namespaced_{}".format(self.kind)):
-                return getattr(self.api, 'replace_namespaced_{}'.format(self.kind))(
-                    name=self.name, body=self.body, namespace=self.namespace)
-
-            return getattr(self.api, 'replace_{}'.format(self.kind))(
-                name=self.name, body=self.body)
-        except ApiException as e:
-            log.error('Exception when calling "replace_namespaced_{}": {}'.format(self.kind, self._add_indent(e.body)))
-            raise ProvisioningError(e)
-
-    def delete(self):
-        try:
-            if hasattr(self.api, "delete_namespaced_{}".format(self.kind)):
-                return getattr(self.api, 'delete_namespaced_{}'.format(self.kind))(
-                    name=self.name, body=client.V1DeleteOptions(propagation_policy='Foreground'),
-                    namespace=self.namespace)
-
-            return getattr(self.api, 'delete_{}'.format(self.kind))(
-                name=self.name, body=client.V1DeleteOptions(propagation_policy='Foreground'))
-        except ApiException as e:
-            if e.reason == 'Not Found':
-                return None
-            log.error('Exception when calling "delete_namespaced_{}": {}'.format(self.kind, self._add_indent(e.body)))
-            raise ProvisioningError(e)
-
-    @staticmethod
-    def _add_indent(json_str):
-        try:
-            return json.dumps(json.loads(json_str), indent=4)
-        except:  # NOQA
-            return json_str
diff --git a/k8s_handle/templating.py b/k8s_handle/templating.py
index 64457ce..f0ed22c 100644
--- a/k8s_handle/templating.py
+++ b/k8s_handle/templating.py
@@ -10,11 +10,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
 from jinja2.exceptions import TemplateNotFound, TemplateSyntaxError, UndefinedError
 
 from k8s_handle import settings
-
-
-class TemplateRenderingError(Exception):
-    pass
-
+from k8s_handle.exceptions import TemplateRenderingError
 
 log = logging.getLogger(__name__)
 
diff --git a/k8s_handle/transforms.py b/k8s_handle/transforms.py
new file mode 100644
index 0000000..d178d3d
--- /dev/null
+++ b/k8s_handle/transforms.py
@@ -0,0 +1,16 @@
+import json
+import re
+
+
+def split_str_by_capital_letters(item):
+    # upper the first letter
+    item = item[0].upper() + item[1:]
+    # transform 'Service' to 'service', 'CronJob' to 'cron_job', 'TargetPort' to 'target_port', etc.
+    return '_'.join(re.findall(r'[A-Z][^A-Z]*', item)).lower()
+
+
+def add_indent(json_str):
+    try:
+        return json.dumps(json.loads(json_str), indent=4)
+    except:  # NOQA
+        return json_str


Test patch:
diff --git a/k8s_handle/k8s/test_adapter.py b/k8s_handle/k8s/test_adapters.py
similarity index 53%
rename from k8s_handle/k8s/test_adapter.py
rename to k8s_handle/k8s/test_adapters.py
index 0111676..435212b 100644
--- a/k8s_handle/k8s/test_adapter.py
+++ b/k8s_handle/k8s/test_adapters.py
@@ -1,31 +1,28 @@
 import unittest
-from .resource import Adapter
-from .mocks import K8sClientMock
 
-from .resource import ProvisioningError
+from kubernetes.client import V1APIResource
 
+from k8s_handle.exceptions import ProvisioningError
+from k8s_handle.transforms import split_str_by_capital_letters
+from .adapters import Adapter, AdapterBuiltinKind, AdapterCustomKind
+from .mocks import K8sClientMock, CustomObjectsAPIMock, ResourcesAPIMock
 
-class TestAdapter(unittest.TestCase):
 
+class TestAdapterBuiltInKind(unittest.TestCase):
     def test_get_app_kind(self):
-        self.assertEqual(Adapter._get_app_kind('ConfigMap'), 'config_map')
-        self.assertEqual(Adapter._get_app_kind('Namespace'), 'namespace')
-        self.assertEqual(Adapter._get_app_kind('PodDisruptionBudget'), 'pod_disruption_budget')
-
-    def test_app_kind_invalid(self):
-        with self.assertRaises(RuntimeError) as context:
-            Adapter(spec={'kind': 'UnknownKind', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
-        self.assertTrue('Unknown kind "UnknownKind" in generated file' in str(context.exception))
+        self.assertEqual(split_str_by_capital_letters('ConfigMap'), 'config_map')
+        self.assertEqual(split_str_by_capital_letters('Namespace'), 'namespace')
+        self.assertEqual(split_str_by_capital_letters('PodDisruptionBudget'), 'pod_disruption_budget')
 
     def test_app_get_fail(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
             deployment.get()
         self.assertTrue('Get deployment fail' in str(context.exception))
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'StorageClass', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
@@ -33,27 +30,27 @@ class TestAdapter(unittest.TestCase):
         self.assertTrue('Get storage class fail' in str(context.exception))
 
     def test_app_get_not_found(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'Deployment', 'metadata': {'name': '404'}, 'spec': {'replicas': 1}})
         res = deployment.get()
         self.assertEqual(res, None)
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'StorageClass', 'metadata': {'name': '404'}, 'spec': {'replicas': 1}})
         res = storage.get()
         self.assertEqual(res, None)
 
     def test_app_get(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock(),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test'}, 'spec': {'replicas': 1}})
         res = deployment.get()
 
         self.assertEqual(res.metadata, {'key1': 'value1'})
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock(),
             spec={'kind': 'StorageClass', 'metadata': {'name': 'test'}, 'spec': {'replicas': 1}})
         res = storage.get()
@@ -61,14 +58,14 @@ class TestAdapter(unittest.TestCase):
         self.assertEqual(res.metadata, {'key1': 'value1'})
 
     def test_app_create_fail(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'Deployment', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
             deployment.create()
         self.assertTrue('Create deployment fail' in str(context.exception))
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'StorageClass', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
@@ -76,62 +73,62 @@ class TestAdapter(unittest.TestCase):
         self.assertTrue('Create storage class fail' in str(context.exception))
 
     def test_app_create(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock(''),
             spec={'kind': 'Deployment', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         res = deployment.create()
         self.assertEqual(res, {'key1': 'value1'})
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock(''),
             spec={'kind': 'StorageClass', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         res = storage.create()
         self.assertEqual(res, {'key1': 'value1'})
 
     def test_app_replace_fail(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
-            deployment.replace()
+            deployment.replace({})
         self.assertTrue('Replace deployment fail' in str(context.exception))
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'StorageClass', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
-            storage.replace()
+            storage.replace({})
         self.assertTrue('Replace storage class fail' in str(context.exception))
 
     def test_app_replace(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock(''),
             spec={'kind': 'Deployment', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
-        res = deployment.replace()
+        res = deployment.replace({})
         self.assertEqual(res, {'key1': 'value1'})
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock(''),
             spec={'kind': 'StorageClass', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
-        res = storage.replace()
+        res = storage.replace({})
         self.assertEqual(res, {'key1': 'value1'})
 
     def test_app_replace_service(self):
-        deployment = Adapter(
+        deployment = AdapterBuiltinKind(
             api=K8sClientMock(''),
             spec={'kind': 'Service', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
-        res = deployment.replace()
+        res = deployment.replace({})
         self.assertEqual(res, {'key1': 'value1'})
 
     def test_app_delete_fail(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
             client.delete()
         self.assertTrue('Delete deployment fail' in str(context.exception))
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('fail'),
             spec={'kind': 'StorageClass', 'metadata': {'name': 'fail'}, 'spec': {'replicas': 1}})
         with self.assertRaises(ProvisioningError) as context:
@@ -139,29 +136,122 @@ class TestAdapter(unittest.TestCase):
         self.assertTrue('Delete storage class fail' in str(context.exception))
 
     def test_app_delete_not_found(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'Deployment', 'metadata': {'name': '404'}, 'spec': {'replicas': 1}})
         res = client.delete()
         self.assertEqual(res, None)
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'StorageClass', 'metadata': {'name': '404'}, 'spec': {'replicas': 1}})
         res = storage.delete()
         self.assertEqual(res, None)
 
     def test_app_delete(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock(),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test'}, 'spec': {'replicas': 1}})
         res = client.delete()
 
         self.assertEqual(res, {'key1': 'value1'})
 
-        storage = Adapter(
+        storage = AdapterBuiltinKind(
             api=K8sClientMock(),
             spec={'kind': 'StorageClass', 'metadata': {'name': 'test'}, 'spec': {'replicas': 1}})
         res = storage.delete()
 
         self.assertEqual(res, {'key1': 'value1'})
+
+
+class TestAdapter(unittest.TestCase):
+    def test_get_instance_custom(self):
+        self.assertIsInstance(
+            Adapter.get_instance({'kind': "CustomKind"}, CustomObjectsAPIMock(), ResourcesAPIMock()),
+            AdapterCustomKind
+        )
+        self.assertIsInstance(
+            Adapter.get_instance({'kind': "CustomKind"}, CustomObjectsAPIMock(), ResourcesAPIMock()),
+            AdapterCustomKind
+        )
+
+    def test_get_instance_test(self):
+        self.assertIsInstance(
+            Adapter.get_instance(
+                {
+                    'kind': Adapter.kinds_builtin[0],
+                    'apiVersion': 'test/test'
+                }
+            ).api, K8sClientMock)
+
+    def test_get_instance_builtin(self):
+        self.assertIsInstance(
+            Adapter.get_instance(
+                {
+                    'kind': Adapter.kinds_builtin[0],
+                    'apiVersion': 'apps/v1beta1'
+                }
+            ), AdapterBuiltinKind)
+
+    def test_get_instance_negative(self):
+        self.assertIsNone(
+            Adapter.get_instance(
+                {
+                    'kind': Adapter.kinds_builtin[0],
+                    'apiVersion': 'unknown'
+                }
+            )
+        )
+
+
+class TestAdapterCustomKind(unittest.TestCase):
+    @staticmethod
+    def _resources_api_mock():
+        return ResourcesAPIMock(
+            'version',
+            'group/version',
+            [V1APIResource(None, 'group', 'kind', 'kinds', True, [], 'kind', [])]
+        )
+
+    def test_initialization_positive(self):
+        adapter = Adapter.get_instance(
+            {
+                'kind': 'kind',
+                'apiVersion': 'group/version',
+                'metadata': {
+                    "namespace": 'test_namespace'
+                }
+            }, CustomObjectsAPIMock(), TestAdapterCustomKind._resources_api_mock()
+        )
+        self.assertEqual(adapter.kind, 'kind')
+        self.assertEqual(adapter.namespace, 'test_namespace')
+        self.assertEqual(adapter.group, 'group')
+        self.assertEqual(adapter.version, 'version')
+        self.assertEqual(adapter.plural, 'kinds')
+        self.assertIsInstance(adapter.api, CustomObjectsAPIMock)
+
+    def test_initialization_kind_missing(self):
+        adapter = Adapter.get_instance({}, CustomObjectsAPIMock(), TestAdapterCustomKind._resources_api_mock())
+        self.assertFalse(adapter.kind)
+        self.assertFalse(adapter.plural)
+
+    def test_initialization_api_version_invalid(self):
+        adapter = Adapter.get_instance({}, CustomObjectsAPIMock(), TestAdapterCustomKind._resources_api_mock())
+        self.assertFalse(adapter.group)
+        self.assertFalse(adapter.version)
+
+        adapter = Adapter.get_instance(
+            {'apiVersion': 'noslash'},
+            CustomObjectsAPIMock(),
+            TestAdapterCustomKind._resources_api_mock()
+        )
+        self.assertFalse(adapter.group)
+        self.assertFalse(adapter.version)
+
+        adapter = Adapter.get_instance(
+            {'apiVersion': 'domain/version/something'},
+            CustomObjectsAPIMock(),
+            ResourcesAPIMock()
+        )
+        self.assertEqual(adapter.group, 'domain')
+        self.assertEqual(adapter.version, 'version/something')
diff --git a/k8s_handle/k8s/test_deprecation_checker.py b/k8s_handle/k8s/test_deprecation_checker.py
index 1c8d5ad..0e8faf9 100644
--- a/k8s_handle/k8s/test_deprecation_checker.py
+++ b/k8s_handle/k8s/test_deprecation_checker.py
@@ -1,6 +1,7 @@
 import unittest
 
-from k8s_handle.k8s.deprecation_checker import ApiDeprecationChecker, DeprecationError
+from k8s_handle.exceptions import DeprecationError
+from k8s_handle.k8s.deprecation_checker import ApiDeprecationChecker
 
 
 class TestApiDeprecationChecker(unittest.TestCase):
diff --git a/k8s_handle/k8s/test_provisioner.py b/k8s_handle/k8s/test_provisioner.py
index f6a4810..4df954e 100644
--- a/k8s_handle/k8s/test_provisioner.py
+++ b/k8s_handle/k8s/test_provisioner.py
@@ -1,14 +1,14 @@
 import unittest
 
 from k8s_handle import settings
+from k8s_handle.exceptions import ProvisioningError
 from k8s_handle.templating import get_template_contexts
+from .adapters import AdapterBuiltinKind
 from .mocks import K8sClientMock
 from .mocks import ServiceMetadata
 from .mocks import ServicePort
 from .mocks import ServiceSpec
-from .resource import Adapter
-from .resource import Provisioner
-from .resource import ProvisioningError
+from .provisioner import Provisioner
 
 
 class TestProvisioner(unittest.TestCase):
@@ -16,7 +16,7 @@ class TestProvisioner(unittest.TestCase):
         settings.GET_ENVIRON_STRICT = False
 
     def test_deployment_wait_complete_fail(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('test1'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
@@ -24,69 +24,69 @@ class TestProvisioner(unittest.TestCase):
         self.assertTrue('Deployment not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_deployment_wait_complete(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('test2'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         Provisioner('deploy', False, None)._wait_deployment_complete(client, tries=1, timeout=0)
 
     def test_statefulset_wait_complete_fail(self):
-        client = Adapter(api=K8sClientMock('test1'),
-                         spec={'kind': 'StatefulSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test1'),
+                                    spec={'kind': 'StatefulSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
             Provisioner('deploy', False, None)._wait_statefulset_complete(client, tries=1, timeout=0)
         self.assertTrue('StatefulSet not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_statefulset_wait_complete(self):
-        client = Adapter(api=K8sClientMock('test2'),
-                         spec={'kind': 'StatefulSet', 'metadata': {'name': ''}, 'spec': {'replicas': 3}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test2'),
+                                    spec={'kind': 'StatefulSet', 'metadata': {'name': ''}, 'spec': {'replicas': 3}})
         Provisioner('deploy', False, None)._wait_statefulset_complete(client, tries=1, timeout=0)
 
     def test_daemonset_wait_complete_fail(self):
-        client = Adapter(api=K8sClientMock('test1'),
-                         spec={'kind': 'DaemonSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test1'),
+                                    spec={'kind': 'DaemonSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
             Provisioner('deploy', False, None)._wait_daemonset_complete(client, tries=1, timeout=0)
         self.assertTrue('DaemonSet not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_daemonset_wait_complete(self):
-        client = Adapter(api=K8sClientMock('test2'),
-                         spec={'kind': 'DaemonSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test2'),
+                                    spec={'kind': 'DaemonSet', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         Provisioner('deploy', False, None)._wait_daemonset_complete(client, tries=1, timeout=0)
 
     def test_job_wait_complete_fail(self):
-        client = Adapter(api=K8sClientMock('test1'),
-                         spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test1'),
+                                    spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
             Provisioner('deploy', False, None)._wait_job_complete(client, tries=1, timeout=0)
 
         self.assertTrue('Job running failed' in str(context.exception))
 
     def test_job_wait_complete_conditions_fail(self):
-        client = Adapter(api=K8sClientMock('test2'),
-                         spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test2'),
+                                    spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
             Provisioner('deploy', False, None)._wait_job_complete(client, tries=1, timeout=0)
         self.assertTrue('Job not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_job_wait_complete(self):
-        client = Adapter(api=K8sClientMock('test3'),
-                         spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test3'),
+                                    spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         Provisioner('deploy', False, None)._wait_job_complete(client, tries=1, timeout=0)
 
     def test_ns_from_template(self):
-        client = Adapter(api=K8sClientMock('test'),
-                         spec={'kind': 'Job', 'metadata': {'name': '', 'namespace': 'test'},
-                               'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test'),
+                                    spec={'kind': 'Job', 'metadata': {'name': '', 'namespace': 'test'},
+                                          'spec': {'replicas': 1}})
         self.assertEqual(client.namespace, 'test')
 
     def test_ns_from_config(self):
         settings.K8S_NAMESPACE = 'namespace'
-        client = Adapter(api=K8sClientMock('test'),
-                         spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
+        client = AdapterBuiltinKind(api=K8sClientMock('test'),
+                                    spec={'kind': 'Job', 'metadata': {'name': ''}, 'spec': {'replicas': 1}})
         self.assertEqual(client.namespace, 'namespace')
 
     def test_deployment_destruction_wait_fail(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('test1'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
@@ -94,13 +94,13 @@ class TestProvisioner(unittest.TestCase):
         self.assertTrue('Deployment destruction not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_deployment_destruction_wait_success(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'Deployment', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         Provisioner('destroy', False, None)._wait_destruction_complete(client, 'Deployment', tries=1, timeout=0)
 
     def test_job_destruction_wait_fail(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('test1'),
             spec={'kind': 'Job', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         with self.assertRaises(RuntimeError) as context:
@@ -108,7 +108,7 @@ class TestProvisioner(unittest.TestCase):
         self.assertTrue('Job destruction not completed for 1 tries' in str(context.exception), context.exception)
 
     def test_job_destruction_wait_success(self):
-        client = Adapter(
+        client = AdapterBuiltinKind(
             api=K8sClientMock('404'),
             spec={'kind': 'Job', 'metadata': {'name': 'test1'}, 'spec': {'replicas': 1}})
         Provisioner('destroy', False, None)._wait_destruction_complete(client, 'Job', tries=1, timeout=0)