From a6ffe49cbac6deb8c28c99617e02ce93a0bf25a2 Mon Sep 17 00:00:00 2001 From: Sam Chow Date: Tue, 28 Aug 2018 13:44:58 -0400 Subject: [PATCH] Extract out deployment rollout and add tests --- config_app/config_endpoints/api/kubeconfig.py | 6 +- config_app/config_util/k8saccessor.py | 133 ++++++++++-------- .../config_util/test/test_k8saccessor.py | 50 ++++++- .../kube-deploy-modal.component.ts | 12 +- config_app/js/services/services.types.ts | 11 +- 5 files changed, 140 insertions(+), 72 deletions(-) diff --git a/config_app/config_endpoints/api/kubeconfig.py b/config_app/config_endpoints/api/kubeconfig.py index e63c3d99e..556e36504 100644 --- a/config_app/config_endpoints/api/kubeconfig.py +++ b/config_app/config_endpoints/api/kubeconfig.py @@ -42,7 +42,11 @@ class QEDeploymentRolloutStatus(ApiResource): @kubernetes_only @nickname('scGetDeploymentRolloutStatus') def get(self, deployment): - return KubernetesAccessorSingleton.get_instance().get_deployment_rollout_status(deployment).to_dict() + deployment_rollout_status = KubernetesAccessorSingleton.get_instance().get_deployment_rollout_status(deployment) + return { + 'status': deployment_rollout_status.status, + 'message': deployment_rollout_status.message, + } @resource('/v1/superuser/config/kubernetes') diff --git a/config_app/config_util/k8saccessor.py b/config_app/config_util/k8saccessor.py index 0ce67953d..90597adfd 100644 --- a/config_app/config_util/k8saccessor.py +++ b/config_app/config_util/k8saccessor.py @@ -5,6 +5,7 @@ import datetime import os from requests import Request, Session +from collections import namedtuple from util.config.validator import EXTRA_CA_DIRECTORY, EXTRA_CA_DIRECTORY_PREFIX from config_app.config_util.k8sconfig import KubernetesConfig @@ -12,22 +13,74 @@ from config_app.config_util.k8sconfig import KubernetesConfig logger = logging.getLogger(__name__) QE_DEPLOYMENT_LABEL = 'quay-enterprise-component' +QE_CONTAINER_NAME = 'quay-enterprise-app' -class _DeploymentRolloutStatus: - """ - Class containing response of the deployment rollout status method. - status is one of: 'failed' | 'progressing' | 'available' - message is any string describing the state. - """ - def __init__(self, status, message): - self.status = status - self.message = message - def to_dict(self): - return { - 'status': self.status, - 'message': self.message - } +# Tuple containing response of the deployment rollout status method. +# status is one of: 'failed' | 'progressing' | 'available' +# message is any string describing the state. +DeploymentRolloutStatus = namedtuple('DeploymentRolloutStatus', ['status', 'message']) + + +def _deployment_rollout_status_message(deployment, deployment_name): + """ + Gets the friendly human readable message of the current state of the deployment rollout + :param deployment: python dict matching: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#deployment-v1-apps + :param deployment_name: string + :return: DeploymentRolloutStatus + """ + # Logic for rollout status pulled from the `kubectl rollout status` command: + # https://github.com/kubernetes/kubernetes/blob/d9ba19c751709c8608e09a0537eea98973f3a796/pkg/kubectl/rollout_status.go#L62 + if deployment['metadata']['generation'] <= deployment['status']['observedGeneration']: + for cond in deployment['status']['conditions']: + if cond['type'] == 'Progressing' and cond['reason'] == 'ProgressDeadlineExceeded': + return DeploymentRolloutStatus( + status='failed', + message="Deployment %s's rollout failed. Please try again later." % deployment_name + ) + + desired_replicas = deployment['spec']['replicas'] + current_replicas = deployment['status'].get('replicas', 0) + if current_replicas == 0: + return DeploymentRolloutStatus( + status='available', + message='Deployment %s updated (no replicas, so nothing to roll out)' % deployment_name + ) + + # Some fields are optional in the spec, so if they're omitted, replace with defaults that won't indicate a wrong status + available_replicas = deployment['status'].get('availableReplicas', 0) + updated_replicas = deployment['status'].get('updatedReplicas', 0) + + if updated_replicas < desired_replicas: + return DeploymentRolloutStatus( + status='progressing', + message='Waiting for rollout to finish: %d out of %d new replicas have been updated...' % ( + updated_replicas, desired_replicas) + ) + + if current_replicas > updated_replicas: + return DeploymentRolloutStatus( + status='progressing', + message='Waiting for rollout to finish: %d old replicas are pending termination...' % ( + current_replicas - updated_replicas) + ) + + if available_replicas < updated_replicas: + return DeploymentRolloutStatus( + status='progressing', + message='Waiting for rollout to finish: %d of %d updated replicas are available...' % ( + available_replicas, updated_replicas) + ) + + return DeploymentRolloutStatus( + status='available', + message='Deployment %s successfully rolled out.' % deployment_name + ) + + return DeploymentRolloutStatus( + status='progressing', + message='Waiting for deployment spec to be updated...' + ) class KubernetesAccessorSingleton(object): @@ -123,55 +176,11 @@ class KubernetesAccessorSingleton(object): response = self._execute_k8s_api('GET', deployment_selector_url, api_prefix='apis/apps/v1') if response.status_code != 200: - return _DeploymentRolloutStatus('failed', 'Could not get deployment. Please check that the deployment exists') + return DeploymentRolloutStatus('failed', 'Could not get deployment. Please check that the deployment exists') + deployment = json.loads(response.text) - # Logic for rollout status pulled from the `kubectl rollout status` command: - # https://github.com/kubernetes/kubernetes/blob/d9ba19c751709c8608e09a0537eea98973f3a796/pkg/kubectl/rollout_status.go#L62 - if deployment['metadata']['generation'] <= deployment['status']['observedGeneration']: - for cond in deployment['status']['conditions']: - if cond['type'] == 'Progressing' and cond['reason'] == 'ProgressDeadlineExceeded': - return _DeploymentRolloutStatus( - 'failed', - 'Deployment %s\'s rollout failed. Please try again later.' % deployment_name - ) - - desired_replicas = deployment['spec']['replicas'] - current_replicas = deployment['status'].get('replicas', 0) - if current_replicas == 0: - return _DeploymentRolloutStatus( - 'available', - 'Deployment %s updated (no replicas, so nothing to roll out)' - ) - # Some fields are optional in the spec, so if they're omitted, replace with defaults that won't indicate a wrong status - available_replicas = deployment['status'].get('availableReplicas', 0) - updated_replicas = deployment['status'].get('updatedReplicas', 0) - - if updated_replicas < desired_replicas: - return _DeploymentRolloutStatus( - 'progressing', - 'Waiting for rollout to finish: %d out of %d new replicas have been updated...' % (updated_replicas, desired_replicas) - ) - if current_replicas > updated_replicas: - return _DeploymentRolloutStatus( - 'progressing', - 'Waiting for rollout to finish: %d old replicas are pending termination...' % (current_replicas - updated_replicas) - ) - if available_replicas < updated_replicas: - return _DeploymentRolloutStatus( - 'progressing', - 'Waiting for rollout to finish: %d of %d updated replicas are available...' % (available_replicas, updated_replicas) - ) - - return _DeploymentRolloutStatus( - 'available', - 'Deployment %s successfully rolled out.' % deployment_name - ) - - return _DeploymentRolloutStatus( - 'progressing', - 'Waiting for deployment spec to be updated...' - ) + return _deployment_rollout_status_message(deployment, deployment_name) def get_qe_deployments(self): """" @@ -205,7 +214,7 @@ class KubernetesAccessorSingleton(object): 'containers': [{ # Note: this name MUST match the deployment template's pod template # (e.g.