Merge "Adding node-labels api"
This commit is contained in:
commit
290448fe83
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
|||||||
/.cache
|
/.cache
|
||||||
/.eggs
|
/.eggs
|
||||||
/.helm-pid
|
/.helm-pid
|
||||||
|
/.pytest_cache
|
||||||
/.tox
|
/.tox
|
||||||
/build
|
/build
|
||||||
/conformance
|
/conformance
|
||||||
|
@ -62,3 +62,33 @@ Responses:
|
|||||||
|
|
||||||
+ 200 OK: Documents were successfully validated
|
+ 200 OK: Documents were successfully validated
|
||||||
+ 400 Bad Request: Documents were not successfully validated
|
+ 400 Bad Request: Documents were not successfully validated
|
||||||
|
|
||||||
|
|
||||||
|
/v1.0/node-labels/<node_name>
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Update node labels
|
||||||
|
|
||||||
|
PUT /v1.0/node-labels/<node_name>
|
||||||
|
|
||||||
|
Updates node labels eg: adding new labels, overriding existing
|
||||||
|
labels and deleting labels from a node.
|
||||||
|
|
||||||
|
Message Body:
|
||||||
|
|
||||||
|
dict of labels
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{"label-a": "value1", "label-b": "value2", "label-c": "value3"}
|
||||||
|
|
||||||
|
Responses:
|
||||||
|
|
||||||
|
+ 200 OK: Labels successfully updated
|
||||||
|
+ 400 Bad Request: Bad input format
|
||||||
|
+ 401 Unauthorized: Unauthenticated access
|
||||||
|
+ 403 Forbidden: Unauthorized access
|
||||||
|
+ 404 Not Found: Bad URL or Node not found
|
||||||
|
+ 500 Internal Server Error: Server error encountered
|
||||||
|
+ 502 Bad Gateway: Kubernetes Config Error
|
||||||
|
+ 503 Service Unavailable: Failed to interact with Kubernetes API
|
||||||
|
@ -39,3 +39,18 @@ Promenade Exceptions
|
|||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
* - KubernetesConfigException
|
||||||
|
- .. autoexception:: promenade.exceptions.KubernetesConfigException
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
|
* - KubernetesApiError
|
||||||
|
- .. autoexception:: promenade.exceptions.KubernetesApiError
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
|
* - NodeNotFoundException
|
||||||
|
- .. autoexception:: promenade.exceptions.NodeNotFoundException
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
|
@ -19,6 +19,7 @@ from promenade.control.health_api import HealthResource
|
|||||||
from promenade.control.join_scripts import JoinScriptsResource
|
from promenade.control.join_scripts import JoinScriptsResource
|
||||||
from promenade.control.middleware import (AuthMiddleware, ContextMiddleware,
|
from promenade.control.middleware import (AuthMiddleware, ContextMiddleware,
|
||||||
LoggingMiddleware)
|
LoggingMiddleware)
|
||||||
|
from promenade.control.node_labels import NodeLabelsResource
|
||||||
from promenade.control.validatedesign import ValidateDesignResource
|
from promenade.control.validatedesign import ValidateDesignResource
|
||||||
from promenade import exceptions as exc
|
from promenade import exceptions as exc
|
||||||
from promenade import logging
|
from promenade import logging
|
||||||
@ -41,6 +42,7 @@ def start_api():
|
|||||||
('/health', HealthResource()),
|
('/health', HealthResource()),
|
||||||
('/join-scripts', JoinScriptsResource()),
|
('/join-scripts', JoinScriptsResource()),
|
||||||
('/validatedesign', ValidateDesignResource()),
|
('/validatedesign', ValidateDesignResource()),
|
||||||
|
('/node-labels/{node_name}', NodeLabelsResource()),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Set up the 1.0 routes
|
# Set up the 1.0 routes
|
||||||
|
44
promenade/control/node_labels.py
Normal file
44
promenade/control/node_labels.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
|
||||||
|
from promenade.control.base import BaseResource
|
||||||
|
from promenade.kubeclient import KubeClient
|
||||||
|
from promenade import exceptions
|
||||||
|
from promenade import logging
|
||||||
|
from promenade import policy
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeLabelsResource(BaseResource):
|
||||||
|
"""Class for Node Labels Manage API"""
|
||||||
|
|
||||||
|
@policy.ApiEnforcer('kubernetes_provisioner:update_node_labels')
|
||||||
|
def on_put(self, req, resp, node_name=None):
|
||||||
|
json_data = self.req_json(req)
|
||||||
|
if node_name is None:
|
||||||
|
LOG.error("Invalid format error: Missing input: node_name")
|
||||||
|
raise exceptions.InvalidFormatError(
|
||||||
|
description="Missing input: node_name")
|
||||||
|
if json_data is None:
|
||||||
|
LOG.error("Invalid format error: Missing input: labels dict")
|
||||||
|
raise exceptions.InvalidFormatError(
|
||||||
|
description="Missing input: labels dict")
|
||||||
|
kubeclient = KubeClient()
|
||||||
|
response = kubeclient.update_node_labels(node_name, json_data)
|
||||||
|
|
||||||
|
resp.body = response
|
||||||
|
resp.status = falcon.HTTP_200
|
@ -295,6 +295,14 @@ class InvalidFormatError(PromenadeException):
|
|||||||
title = 'Invalid Input Error'
|
title = 'Invalid Input Error'
|
||||||
status = falcon.HTTP_400
|
status = falcon.HTTP_400
|
||||||
|
|
||||||
|
def __init__(self, title="", description=""):
|
||||||
|
if not title:
|
||||||
|
title = self.title
|
||||||
|
if not description:
|
||||||
|
description = self.title
|
||||||
|
super(InvalidFormatError, self).__init__(
|
||||||
|
title, description, status=self.status)
|
||||||
|
|
||||||
|
|
||||||
class ValidationException(PromenadeException):
|
class ValidationException(PromenadeException):
|
||||||
"""
|
"""
|
||||||
@ -320,6 +328,21 @@ class TemplateRenderException(PromenadeException):
|
|||||||
status = falcon.HTTP_500
|
status = falcon.HTTP_500
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesConfigException(PromenadeException):
|
||||||
|
title = 'Kubernetes Config Error'
|
||||||
|
status = falcon.HTTP_502
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesApiError(PromenadeException):
|
||||||
|
title = 'Kubernetes API Error'
|
||||||
|
status = falcon.HTTP_503
|
||||||
|
|
||||||
|
|
||||||
|
class NodeNotFoundException(KubernetesApiError):
|
||||||
|
title = 'Node not found'
|
||||||
|
status = falcon.HTTP_404
|
||||||
|
|
||||||
|
|
||||||
def massage_error_list(error_list, placeholder_description):
|
def massage_error_list(error_list, placeholder_description):
|
||||||
"""
|
"""
|
||||||
Returns a best-effort attempt to make a nice error list
|
Returns a best-effort attempt to make a nice error list
|
||||||
|
136
promenade/kubeclient.py
Normal file
136
promenade/kubeclient.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
import kubernetes
|
||||||
|
from kubernetes.client.rest import ApiException
|
||||||
|
from urllib3.exceptions import MaxRetryError
|
||||||
|
|
||||||
|
from promenade import logging
|
||||||
|
from promenade.exceptions import KubernetesApiError
|
||||||
|
from promenade.exceptions import KubernetesConfigException
|
||||||
|
from promenade.exceptions import NodeNotFoundException
|
||||||
|
from promenade.utils.success_message import SuccessMessage
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KubeClient(object):
|
||||||
|
"""
|
||||||
|
Class for Kubernetes APIs client
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
""" Set Kubernetes APIs connection """
|
||||||
|
try:
|
||||||
|
LOG.info('Loading in-cluster Kubernetes configuration.')
|
||||||
|
kubernetes.config.load_incluster_config()
|
||||||
|
except kubernetes.config.config_exception.ConfigException:
|
||||||
|
LOG.debug('Failed to load in-cluster configuration')
|
||||||
|
try:
|
||||||
|
LOG.info('Loading out-of-cluster Kubernetes configuration.')
|
||||||
|
kubernetes.config.load_kube_config()
|
||||||
|
except FileNotFoundError:
|
||||||
|
LOG.exception(
|
||||||
|
'FileNotFoundError: Failed to load Kubernetes config file.'
|
||||||
|
)
|
||||||
|
raise KubernetesConfigException
|
||||||
|
self.client = kubernetes.client.CoreV1Api()
|
||||||
|
|
||||||
|
def update_node_labels(self, node_name, input_labels):
|
||||||
|
"""
|
||||||
|
Updating node labels
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node_name(str): node for which updating labels
|
||||||
|
input_labels(dict): input labels dict
|
||||||
|
Returns:
|
||||||
|
SuccessMessage(dict): API success response
|
||||||
|
"""
|
||||||
|
resp_body_succ = SuccessMessage('Update node labels', falcon.HTTP_200)
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_labels = self.get_node_labels(node_name)
|
||||||
|
update_labels = _get_update_labels(existing_labels, input_labels)
|
||||||
|
# If there is a change
|
||||||
|
if bool(update_labels):
|
||||||
|
body = {"metadata": {"labels": update_labels}}
|
||||||
|
self.client.patch_node(node_name, body)
|
||||||
|
return resp_body_succ.get_output_json()
|
||||||
|
except (ApiException, MaxRetryError) as e:
|
||||||
|
LOG.exception(
|
||||||
|
"An exception occurred during node labels update: " + str(e))
|
||||||
|
raise KubernetesApiError
|
||||||
|
|
||||||
|
def get_node_labels(self, node_name):
|
||||||
|
"""
|
||||||
|
Get existing registered node labels
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node_name(str): node of which getting labels
|
||||||
|
Returns:
|
||||||
|
dict: labels dict
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.read_node(node_name)
|
||||||
|
if response is not None:
|
||||||
|
return response.metadata.labels
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
except (ApiException, MaxRetryError) as e:
|
||||||
|
LOG.exception(
|
||||||
|
"An exception occurred in fetching node labels: " + str(e))
|
||||||
|
if hasattr(e, 'status') and str(e.status) == "404":
|
||||||
|
raise NodeNotFoundException
|
||||||
|
else:
|
||||||
|
raise KubernetesApiError
|
||||||
|
|
||||||
|
|
||||||
|
def _get_update_labels(existing_labels, input_labels):
|
||||||
|
"""
|
||||||
|
Helper function to add new labels, delete labels, override
|
||||||
|
existing labels
|
||||||
|
|
||||||
|
Args:
|
||||||
|
existing_labels(dict): Existing node labels
|
||||||
|
input_labels(dict): Input/Req. labels
|
||||||
|
Returns:
|
||||||
|
update_labels(dict): Node labels to be updated
|
||||||
|
or
|
||||||
|
input_labels(dict): Node labels to be updated
|
||||||
|
"""
|
||||||
|
update_labels = {}
|
||||||
|
|
||||||
|
# no existing labels found
|
||||||
|
if not existing_labels:
|
||||||
|
# filter delete label request since there is no labels set on a node
|
||||||
|
update_labels.update(
|
||||||
|
{k: v
|
||||||
|
for k, v in input_labels.items() if v is not None})
|
||||||
|
return update_labels
|
||||||
|
|
||||||
|
# new labels or overriding labels
|
||||||
|
update_labels.update({
|
||||||
|
k: v
|
||||||
|
for k, v in input_labels.items()
|
||||||
|
if k not in existing_labels or v != existing_labels[k]
|
||||||
|
})
|
||||||
|
|
||||||
|
# deleted labels
|
||||||
|
update_labels.update({
|
||||||
|
k: None
|
||||||
|
for k in existing_labels.keys()
|
||||||
|
if k not in input_labels and "kubernetes.io" not in k
|
||||||
|
})
|
||||||
|
return update_labels
|
@ -41,6 +41,12 @@ POLICIES = [
|
|||||||
'path': '/api/v1.0/validatedesign',
|
'path': '/api/v1.0/validatedesign',
|
||||||
'method': 'POST'
|
'method': 'POST'
|
||||||
}]),
|
}]),
|
||||||
|
op.DocumentedRuleDefault('kubernetes_provisioner:update_node_labels',
|
||||||
|
'role:admin', 'Update Node Labels',
|
||||||
|
[{
|
||||||
|
'path': '/api/v1.0/node-labels/{node_name}',
|
||||||
|
'method': 'PUT'
|
||||||
|
}]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
37
promenade/utils/message.py
Normal file
37
promenade/utils/message.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Message(object):
|
||||||
|
"""Message base class"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.error_count = 0
|
||||||
|
self.details = {'errorCount': 0, 'messageList': []}
|
||||||
|
self.output = {
|
||||||
|
'kind': 'Status',
|
||||||
|
'apiVersion': 'v1.0',
|
||||||
|
'metadata': {},
|
||||||
|
'details': self.details
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_output_json(self):
|
||||||
|
"""Returns message as JSON.
|
||||||
|
|
||||||
|
:returns: Message formatted in JSON.
|
||||||
|
:rtype: json
|
||||||
|
"""
|
||||||
|
return json.dumps(self.output, indent=2)
|
32
promenade/utils/success_message.py
Normal file
32
promenade/utils/success_message.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
|
||||||
|
from promenade.utils.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class SuccessMessage(Message):
|
||||||
|
"""SuccessMessage per UCP convention:
|
||||||
|
https://airshipit.readthedocs.io/en/latest/api-conventions.html#status-responses
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, reason='', code=falcon.HTTP_200):
|
||||||
|
super(SuccessMessage, self).__init__()
|
||||||
|
self.output.update({
|
||||||
|
'status': 'Success',
|
||||||
|
'message': '',
|
||||||
|
'reason': reason,
|
||||||
|
'code': code
|
||||||
|
})
|
@ -15,8 +15,10 @@
|
|||||||
import falcon
|
import falcon
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from promenade.utils.message import Message
|
||||||
|
|
||||||
class ValidationMessage(object):
|
|
||||||
|
class ValidationMessage(Message):
|
||||||
""" ValidationMessage per UCP convention:
|
""" ValidationMessage per UCP convention:
|
||||||
https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure # noqa
|
https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure # noqa
|
||||||
|
|
||||||
@ -34,15 +36,8 @@ class ValidationMessage(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.error_count = 0
|
super(ValidationMessage, self).__init__()
|
||||||
self.details = {'errorCount': 0, 'messageList': []}
|
self.output.update({'reason': 'Validation'})
|
||||||
self.output = {
|
|
||||||
'kind': 'Status',
|
|
||||||
'apiVersion': 'v1.0',
|
|
||||||
'metadata': {},
|
|
||||||
'reason': 'Validation',
|
|
||||||
'details': self.details,
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_error_message(self,
|
def add_error_message(self,
|
||||||
msg,
|
msg,
|
||||||
@ -80,14 +75,6 @@ class ValidationMessage(object):
|
|||||||
self.output['status'] = 'Success'
|
self.output['status'] = 'Success'
|
||||||
return self.output
|
return self.output
|
||||||
|
|
||||||
def get_output_json(self):
|
|
||||||
""" Return ValidationMessage message as JSON.
|
|
||||||
|
|
||||||
:returns: The ValidationMessage formatted in JSON, for logging.
|
|
||||||
:rtype: json
|
|
||||||
"""
|
|
||||||
return json.dumps(self.output, indent=2)
|
|
||||||
|
|
||||||
def update_response(self, resp):
|
def update_response(self, resp):
|
||||||
output = self.get_output()
|
output = self.get_output()
|
||||||
resp.status = output['code']
|
resp.status = output['code']
|
||||||
|
101
tests/unit/api/test_kubeclient.py
Normal file
101
tests/unit/api/test_kubeclient.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from promenade import kubeclient
|
||||||
|
|
||||||
|
TEST_DATA = [(
|
||||||
|
'Multi-facet update',
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-c": "value4",
|
||||||
|
"label-d": "value99",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-b": None,
|
||||||
|
"label-c": "value4",
|
||||||
|
"label-d": "value99",
|
||||||
|
},
|
||||||
|
), (
|
||||||
|
'Add labels when none exist',
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
), (
|
||||||
|
'No updates',
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
), (
|
||||||
|
'Delete labels',
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"label-c": "value3",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"label-a": None,
|
||||||
|
"label-b": None,
|
||||||
|
"label-c": None,
|
||||||
|
},
|
||||||
|
), (
|
||||||
|
'Delete labels when none',
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
), (
|
||||||
|
'Avoid kubernetes.io labels Deletion',
|
||||||
|
{
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-b": "value2",
|
||||||
|
"kubernetes.io/hostname": "ubutubox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-a": "value99",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label-a": "value99",
|
||||||
|
"label-b": None,
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('description,existing_lbl,input_lbl,expected',
|
||||||
|
TEST_DATA)
|
||||||
|
def test_get_update_labels(description, existing_lbl, input_lbl, expected):
|
||||||
|
applied = kubeclient._get_update_labels(existing_lbl, input_lbl)
|
||||||
|
assert applied == expected
|
88
tests/unit/api/test_update_labels.py
Normal file
88
tests/unit/api/test_update_labels.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from falcon import testing
|
||||||
|
from promenade import promenade
|
||||||
|
from promenade.utils.success_message import SuccessMessage
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client():
|
||||||
|
return testing.TestClient(promenade.start_promenade(disable='keystone'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def req_header():
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-IDENTITY-STATUS': 'Confirmed',
|
||||||
|
'X-USER-NAME': 'Test',
|
||||||
|
'X-ROLES': 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def req_body():
|
||||||
|
return json.dumps({
|
||||||
|
"label-a": "value1",
|
||||||
|
"label-c": "value4",
|
||||||
|
"label-d": "value99"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('promenade.kubeclient.KubeClient.update_node_labels')
|
||||||
|
@mock.patch('promenade.kubeclient.KubeClient.__init__')
|
||||||
|
def test_node_labels_pass(mock_kubeclient, mock_update_node_labels, client,
|
||||||
|
req_header, req_body):
|
||||||
|
"""
|
||||||
|
Function to test node labels pass test case
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mock_kubeclient: mock KubeClient object
|
||||||
|
mock_update_node_labels: mock update_node_labels object
|
||||||
|
client: Promenode APIs test client
|
||||||
|
req_header: API request header
|
||||||
|
req_body: API request body
|
||||||
|
"""
|
||||||
|
mock_kubeclient.return_value = None
|
||||||
|
mock_update_node_labels.return_value = _mock_update_node_labels()
|
||||||
|
response = client.simulate_put(
|
||||||
|
'/api/v1.0/node-labels/ubuntubox', headers=req_header, body=req_body)
|
||||||
|
assert response.status == falcon.HTTP_200
|
||||||
|
assert response.json["status"] == "Success"
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_labels_missing_inputs(client, req_header, req_body):
|
||||||
|
"""
|
||||||
|
Function to test node labels missing inputs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Promenode APIs test client
|
||||||
|
req_header: API request header
|
||||||
|
req_body: API request body
|
||||||
|
"""
|
||||||
|
response = client.simulate_post(
|
||||||
|
'/api/v1.0/node-labels', headers=req_header, body=req_body)
|
||||||
|
assert response.status == falcon.HTTP_404
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_update_node_labels():
|
||||||
|
"""Mock update_node_labels function"""
|
||||||
|
resp_body_succ = SuccessMessage('Update node labels')
|
||||||
|
return resp_body_succ.get_output_json()
|
@ -61,3 +61,8 @@ promenade_health_check() {
|
|||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promenade_put_labels_url() {
|
||||||
|
NODE_NAME=${1}
|
||||||
|
echo "${PROMENADE_BASE_URL}/api/v1.0/node-labels/${NODE_NAME}"
|
||||||
|
}
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -eu
|
||||||
|
|
||||||
source "${GATE_UTILS}"
|
source "${GATE_UTILS}"
|
||||||
|
|
||||||
log Adding labels to node n0
|
VIA="n1"
|
||||||
kubectl_cmd n1 label node n0 \
|
|
||||||
calico-etcd=enabled \
|
|
||||||
kubernetes-apiserver=enabled \
|
|
||||||
kubernetes-controller-manager=enabled \
|
|
||||||
kubernetes-etcd=enabled \
|
|
||||||
kubernetes-scheduler=enabled
|
|
||||||
|
|
||||||
# XXX Need to wait
|
CURL_ARGS=("--fail" "--max-time" "300" "--retry" "16" "--retry-delay" "15")
|
||||||
|
|
||||||
|
log Adding labels to node n0
|
||||||
|
JSON="{\"calico-etcd\": \"enabled\", \"coredns\": \"enabled\", \"kubernetes-apiserver\": \"enabled\", \"kubernetes-controller-manager\": \"enabled\", \"kubernetes-etcd\": \"enabled\", \"kubernetes-scheduler\": \"enabled\", \"ucp-control-plane\": \"enabled\"}"
|
||||||
|
|
||||||
|
ssh_cmd "${VIA}" curl -v "${CURL_ARGS[@]}" -X PUT -H "Content-Type: application/json" -d "${JSON}" "$(promenade_put_labels_url n0)"
|
||||||
|
|
||||||
|
# Need to wait
|
||||||
sleep 60
|
sleep 60
|
||||||
|
|
||||||
validate_etcd_membership kubernetes n1 n0 n1 n2 n3
|
validate_etcd_membership kubernetes n1 n0 n1 n2 n3
|
||||||
validate_etcd_membership calico n1 n0 n1 n2 n3
|
validate_etcd_membership calico n1 n0 n1 n2 n3
|
||||||
|
|
||||||
log Removing labels from node n2
|
log Removing labels from node n2
|
||||||
kubectl_cmd n1 label node n2 \
|
JSON="{\"coredns\": \"enabled\", \"ucp-control-plane\": \"enabled\"}"
|
||||||
calico-etcd- \
|
|
||||||
kubernetes-apiserver- \
|
|
||||||
kubernetes-controller-manager- \
|
|
||||||
kubernetes-etcd- \
|
|
||||||
kubernetes-scheduler-
|
|
||||||
|
|
||||||
# XXX Need to wait
|
ssh_cmd "${VIA}" curl -v "${CURL_ARGS[@]}" -X PUT -H "Content-Type: application/json" -d "${JSON}" "$(promenade_put_labels_url n2)"
|
||||||
|
|
||||||
|
# Need to wait
|
||||||
sleep 60
|
sleep 60
|
||||||
|
|
||||||
validate_cluster n1
|
validate_cluster n1
|
||||||
|
Loading…
Reference in New Issue
Block a user