Skip to content

Commit

Permalink
Rd 6692 kube client (#219)
Browse files Browse the repository at this point in the history
* getting stuff ready

* pushing first

* update client

* improved

* test changes

* fixing up tests
  • Loading branch information
EarthmanT authored Jan 31, 2023
1 parent c8f2bd1 commit 32f28b1
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 159 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ local-storage/
.cloudify/
**/*.wgn

.vs/
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,4 @@
2.13.19: Update google auth library.
2.13.20: Remove constraints in 1 4 plugin YAML and improve node instance resolution.
2.13.21: RD-6518 - K8s LB Service Endpoint IP not available in Cloudify with AKS.
2.13.22: RD-6692 Update Kubernetes Client
65 changes: 30 additions & 35 deletions cloudify_kubernetes/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,21 @@
handle_existing_resource,
generate_traceback_exception,
NODE_PROPERTY_FILE_RESOURCE_PATH,
create_tempfiles_for_certs_and_keys,
delete_tempfiles_for_certs_and_keys,
INSTANCE_RUNTIME_PROPERTY_KUBERNETES)
from .k8s import (CloudifyKubernetesClient,
KuberentesMappingNotFoundError,
KuberentesInvalidApiClassError,
KuberentesInvalidApiMethodError,
KubernetesApiConfigurationVariants,
KuberentesInvalidPayloadClassError,
KubernetesApiAuthenticationVariants)
KuberentesInvalidPayloadClassError)


from cloudify_kubernetes_sdk.connection.decorators import setup_configuration
from cloudify_kubernetes_sdk.connection.utils import (
get_connection_details_from_shared_cluster,
get_auth_token,
get_host,
get_kubeconfig_file,
get_ssl_ca_file)

NODE_PROPERTY_AUTHENTICATION = 'authentication'
NODE_PROPERTY_CONFIGURATION = 'configuration'
Expand Down Expand Up @@ -257,47 +262,37 @@ def nested_ops():

def with_kubernetes_client(fn):
def wrapper(**kwargs):
client_config_from_inputs_relationships = get_client_config(**kwargs)
configuration_property = _retrieve_property(
ctx,
NODE_PROPERTY_CONFIGURATION,
client_config_from_inputs_relationships
)

authentication_property = _retrieve_property(
ctx,
NODE_PROPERTY_AUTHENTICATION,
client_config_from_inputs_relationships
)

configuration_property = create_tempfiles_for_certs_and_keys(
configuration_property)
client_config = get_client_config(**kwargs)
shared_cluster = get_connection_details_from_shared_cluster()
token = get_auth_token(client_config, shared_cluster.get('api_key'))
host = get_host(client_config, shared_cluster.get('host'))
ca_file = get_ssl_ca_file(client_config,
shared_cluster.get('ssl_ca_cert'))
kubeconfig = get_kubeconfig_file(client_config,
ctx.logger,
ctx.download_resource)

try:
api_client = setup_configuration(
token=token,
host=host,
ca_file=ca_file,
kubeconfig=kubeconfig)
if not api_client:
raise NonRecoverableError(
'Failed to initialize client. '
'Check debug log for more information.')
kwargs['client'] = CloudifyKubernetesClient(
ctx.logger,
KubernetesApiConfigurationVariants(
ctx.logger,
configuration_property,
download_resource=ctx.download_resource
),
KubernetesApiAuthenticationVariants(
ctx.logger,
authentication_property
)
)
ctx.logger, api_client=api_client)

result = fn(**kwargs)
except (RecoverableError, NonRecoverableError):
raise
except Exception:
ctx.logger.info(str(generate_traceback_exception()))
except BaseException:
raise RecoverableError(
'Error encountered',
causes=[generate_traceback_exception()]
)
finally:
delete_tempfiles_for_certs_and_keys(configuration_property)
return result

return operation(func=wrapper, resumable=True)
74 changes: 41 additions & 33 deletions cloudify_kubernetes/k8s/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,35 +122,43 @@ def to_dict(self):

class CloudifyKubernetesClient(object):

def __init__(self, logger, api_configuration, api_authentication=None):
def __init__(self,
logger,
api_configuration=None,
api_authentication=None,
api_client=None):

self.logger = logger
prepare_api = api_configuration.prepare_api()
if isinstance(prepare_api, kubernetes.client.Configuration):
self.client = api_client
self.api = kubernetes.client

if api_authentication:
self.configuration = api_authentication.authenticate(
prepare_api)
else:
self.configuration = prepare_api
if not self.client:
self.logger.debug(
'Deprecated client initialization. Please contact support.')
self.configuration = None
prepare_api = api_configuration.prepare_api()
if isinstance(prepare_api, kubernetes.client.Configuration):

self.api = kubernetes.client
self.api.configuration = \
kubernetes.client.Configuration.set_default(
self.configuration)
self.client = kubernetes.client.ApiClient(
configuration=self.api.configuration)
if api_authentication:
self.configuration = api_authentication.authenticate(
prepare_api)
else:
self.configuration = prepare_api

else:
self.api.configuration = \
kubernetes.client.Configuration.set_default(
self.configuration)
self.client = kubernetes.client.ApiClient(
configuration=self.api.configuration)

if prepare_api:
self.api = prepare_api
else:
self.api = kubernetes.client

self.configuration = None
self.client = self.api.ApiClient()
if prepare_api:
self.api = prepare_api

self.logger.info('Kubernetes API initialized successfully.')
self.client = self.api.ApiClient()

self.logger.debug('Kubernetes API initialized successfully.')

@property
def _name(self):
Expand All @@ -164,7 +172,7 @@ def _prepare_payload(self, class_name, resource_definition):
'Cannot create instance of Kubernetes API payload class: {0}. '
'Class not supported by client {1}'
.format(class_name, self._name))
self.logger.info('Kubernetes API initialized successfully')
self.logger.debug('Kubernetes API initialized successfully')
return getattr(self.api, class_name)(**vars(resource_definition))

def _prepare_api_method(self, class_name, method_name):
Expand All @@ -191,9 +199,9 @@ def _prepare_operation(self, operation, api, method, **_):
api_method, api_method_arguments_names = self._prepare_api_method(
api, method
)
self.logger.info('Preparing operation with api method: {0} '
'(mandatory arguments: {1})'
.format(api_method, api_method_arguments_names))
self.logger.debug('Preparing operation with api method: {0} '
'(mandatory arguments: {1})'
.format(api_method, api_method_arguments_names))

return operation(api_method, api_method_arguments_names)

Expand All @@ -219,23 +227,23 @@ def _execute(self, operation, arguments):
for k, v in list(arguments.items()):
if not v:
del arguments[k]
try:
self.logger.info('Executing operation {0}'.format(operation))
self.logger.info('Executing operation arguments {0}'.format(
self.logger.debug('Executing operation {0}'.format(operation))
self.logger.debug('Executing operation arguments {0}'.format(
arguments))
try:
result = operation.execute(arguments)
self.logger.info('Operation executed successfully')
self.logger.debug('Result: {0}'.format(result))

return result
except kubernetes.client.rest.ApiException as e:
except Exception as e:
if 'the namespace of the provided object does not match ' \
'the namespace sent on the request' in text_type(e):
raise NonRecoverableError(text_type(e))
raise KuberentesApiOperationError(
'Exception during Kubernetes API call: {0}'.format(
text_type(e))
)
self.logger.debug('Operation executed successfully')
self.logger.debug('Result: {0}'.format(result))

return result

def match_namespace(self, resource_definition, options):
namespace_from_def = resource_definition.metadata.get('namespace')
Expand Down
3 changes: 1 addition & 2 deletions cloudify_kubernetes/k8s/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ def _prepare_arguments(self, arguments):
raise KuberentesApiOperationError(
'Invalid input data for execution of Kubernetes API '
'method: input argument {0} is not defined but is '
'mandatory'
.format(mandatory_argument_name))
'mandatory'.format(mandatory_argument_name))

for optional_argument_name in self.API_ACCEPTED_ARGUMENTS:
if optional_argument_name in arguments:
Expand Down
81 changes: 35 additions & 46 deletions cloudify_kubernetes/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _prepare_master_node(self, with_client_config=False,
node = MagicMock()
node.properties = {
'configuration': {
'blueprint_file_name': 'kubernetes.conf'
'blueprint_file_name': 'kubernetes.conf',
'api_options': {},
}
}

Expand Down Expand Up @@ -216,51 +217,47 @@ def test_retrieve_property_with_relationship_to_master(self):
_, _ctx = self._prepare_master_node()
self.assertEqual(
decorators._retrieve_property(_ctx, 'configuration'),
{'blueprint_file_name': 'kubernetes.conf'}
{'api_options': {}, 'blueprint_file_name': 'kubernetes.conf'}
)

def test_retrieve_property_with_client_config(self):
_, _ctx = self._prepare_master_node(with_client_config=True,
with_relationship_to_master=False)
self.assertEqual(
decorators._retrieve_property(_ctx, 'configuration'),
{'blueprint_file_name': 'kubernetes.conf'}
{'api_options': {}, 'blueprint_file_name': 'kubernetes.conf'}
)

def test_with_kubernetes_client_NonRecoverableError(self):
@patch('cloudify_kubernetes.decorators.'
'setup_configuration')
def test_with_kubernetes_client_NonRecoverableError(self, setup):
setup.return_value = True
_, _ctx = self._prepare_master_node()

mock_isfile = MagicMock(return_value=True)

_ctx.download_resource = MagicMock(return_value="downloaded_resource")

with patch('os.path.isfile', mock_isfile):
with patch(
'cloudify_kubernetes.k8s.config.'
'kubernetes.config.load_kube_config',
MagicMock()
):
with self.assertRaises(NonRecoverableError) as error:
decorators.with_kubernetes_client(
MagicMock(side_effect=NonRecoverableError(
'error_text')))()
with self.assertRaises(NonRecoverableError) as error:
decorators.with_kubernetes_client(
MagicMock(side_effect=NonRecoverableError(
'error_text')))()

self.assertEqual(
str(error.exception),
"error_text"
)
self.assertEqual(
str(error.exception),
"error_text"
)

def test_with_kubernetes_client_Exception(self):
_, _ctx = self._prepare_master_node()
@patch('cloudify_kubernetes.decorators.'
'setup_configuration')
def test_with_kubernetes_client_Exception(self, setup):
setup.return_value = True
_ctx = self._prepare_master_node()[1]

mock_isfile = MagicMock(return_value=True)

_ctx.download_resource = MagicMock(return_value="downloaded_resource")

with patch('os.path.isfile', mock_isfile):
with patch(
'cloudify_kubernetes.k8s.config.'
'kubernetes.config.load_kube_config',
'cloudify_kubernetes_sdk.connection.decorators.'
'config.new_client_from_config',
MagicMock()
):
with self.assertRaises(RecoverableError) as error:
Expand All @@ -274,38 +271,30 @@ def test_with_kubernetes_client_Exception(self):
)

def test_with_kubernetes_client_RecoverableError(self):
_, _ctx = self._prepare_master_node()
_ = self._prepare_master_node()[0]

def function(client, **kwargs):
return client, kwargs

with self.assertRaises(RecoverableError) as error:
decorators.with_kubernetes_client(function)()

self.assertEqual(
error.exception.causes[0]['message'],
"Cannot initialize Kubernetes API - no suitable configuration "
"variant found for {'blueprint_file_name': 'kubernetes.conf'} "
"properties"
)
self.assertEqual(
error.exception.causes[0]['message'],
"Error encountered"
)

def test_with_kubernetes_client(self):
@patch('cloudify_kubernetes.decorators.'
'setup_configuration')
def test_with_kubernetes_client(self, setup):
_, _ctx = self._prepare_master_node()

mock_isfile = MagicMock(return_value=True)

setup.return_value = True
_ctx.download_resource = MagicMock(return_value="downloaded_resource")

def function(client, **kwargs):
self.assertTrue(isinstance(client, CloudifyKubernetesClient))

with patch('os.path.isfile', mock_isfile):
with patch(
'cloudify_kubernetes.k8s.config.'
'kubernetes.config.load_kube_config',
MagicMock()
):
decorators.with_kubernetes_client(function)()
decorators.with_kubernetes_client(function)()

def test_with_kubernetes_client_certificate_files(self):
_, _ctx = self._prepare_master_node(with_relationship_to_master=False,
Expand Down Expand Up @@ -333,8 +322,8 @@ def function(client, **kwargs):

with patch('os.path.isfile', mock_isfile):
with patch(
'cloudify_kubernetes.k8s.config.'
'kubernetes.config.load_kube_config',
'cloudify_kubernetes_sdk.connection.decorators.'
'config.new_client_from_config',
MagicMock()
):
decorators.with_kubernetes_client(function)()
Expand Down
Loading

0 comments on commit 32f28b1

Please sign in to comment.