diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 330f51e80..fd2ad8e62 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -281,9 +281,10 @@ def restart(self, **kwargs): # noqa desired = 0 labels = self._scheduler_filter(**kwargs) # fetch RS (which represent Deployments) - controllers = self._scheduler.rs.get(kwargs['id'], labels=labels) - - for controller in controllers.json()['items']: + controllers = self._scheduler.rs.get(kwargs['id'], labels=labels).json()['items'] + if not controllers: + controllers = [] + for controller in controllers: desired += controller['spec']['replicas'] except KubeException: # Nothing was found @@ -737,6 +738,8 @@ def list_pods(self, *args, **kwargs): pods = [self._scheduler.pod.get(self.id, kwargs['name']).json()] else: pods = self._scheduler.pod.get(self.id, labels=labels).json()['items'] + if not pods: + pods = [] data = [] for p in pods: diff --git a/rootfs/api/models/release.py b/rootfs/api/models/release.py index 81e56c291..1ccbbd436 100644 --- a/rootfs/api/models/release.py +++ b/rootfs/api/models/release.py @@ -263,8 +263,10 @@ def cleanup_old(self): # noqa # Cleanup controllers labels = {'heritage': 'deis'} controller_removal = [] - controllers = self._scheduler.rc.get(self.app.id, labels=labels).json() - for controller in controllers['items']: + controllers = self._scheduler.rc.get(self.app.id, labels=labels).json()['items'] + if not controllers: + controllers = [] + for controller in controllers: current_version = controller['metadata']['labels']['version'] # skip the latest release if current_version == latest_version: @@ -289,8 +291,10 @@ def cleanup_old(self): # noqa # Remove stray pods labels = {'heritage': 'deis'} - pods = self._scheduler.pod.get(self.app.id, labels=labels).json() - for pod in pods['items']: + pods = self._scheduler.pod.get(self.app.id, labels=labels).json()['items'] + if not pods: + pods = [] + for pod in pods: if self._scheduler.pod.deleted(pod): continue @@ -319,8 +323,10 @@ def _cleanup_deployment_secrets_and_configs(self, namespace): # Find all ReplicaSets versions = [] labels = {'heritage': 'deis', 'app': namespace} - replicasets = self._scheduler.rs.get(namespace, labels=labels).json() - for replicaset in replicasets['items']: + replicasets = self._scheduler.rs.get(namespace, labels=labels).json()['items'] + if not replicasets: + replicasets = [] + for replicaset in replicasets: if ( 'version' not in replicaset['metadata']['labels'] or replicaset['metadata']['labels']['version'] in versions @@ -338,8 +344,10 @@ def _cleanup_deployment_secrets_and_configs(self, namespace): 'version__notin': versions } self.app.log('Cleaning up orphaned env var secrets for application {}'.format(namespace), level=logging.DEBUG) # noqa - secrets = self._scheduler.secret.get(namespace, labels=labels).json() - for secret in secrets['items']: + secrets = self._scheduler.secret.get(namespace, labels=labels).json()['items'] + if not secrets: + secrets = [] + for secret in secrets: self._scheduler.secret.delete(namespace, secret['metadata']['name']) def _delete_release_in_scheduler(self, namespace, version): @@ -358,8 +366,10 @@ def _delete_release_in_scheduler(self, namespace, version): # see if the app config has deploy timeout preference, otherwise use global timeout = self.config.values.get('DEIS_DEPLOY_TIMEOUT', settings.DEIS_DEPLOY_TIMEOUT) - controllers = self._scheduler.rc.get(namespace, labels=labels).json() - for controller in controllers['items']: + controllers = self._scheduler.rc.get(namespace, labels=labels).json()['items'] + if not controllers: + controllers = [] + for controller in controllers: # Deployment takes care of this in the API, RC does not # Have the RC scale down pods and delete itself self._scheduler.rc.scale(namespace, controller['metadata']['name'], 0, timeout) diff --git a/rootfs/scheduler/mock.py b/rootfs/scheduler/mock.py index 025c99035..be608c50e 100644 --- a/rootfs/scheduler/mock.py +++ b/rootfs/scheduler/mock.py @@ -605,6 +605,10 @@ def fetch_all(request, context): filters = prepare_query_filters(url.query) cache_path = cache_key(request.path) data = filter_data(filters, cache_path) + # HACK(bacongobbler): in kubernetes v1.5, fetching a list of resources returns a NoneType + # instead of an empty list. + if not data: + data = None return {'items': data} diff --git a/rootfs/scheduler/resources/deployment.py b/rootfs/scheduler/resources/deployment.py index 536b229a8..b0a4418af 100644 --- a/rootfs/scheduler/resources/deployment.py +++ b/rootfs/scheduler/resources/deployment.py @@ -224,7 +224,7 @@ def in_progress(self, namespace, name, timeout, batches, replicas, tags): # fetch the latest RS for Deployment and use the start time to compare to deploy timeout replicasets = self.rs.get(namespace, labels=labels).json()['items'] # the labels should ensure that only 1 replicaset due to the version label - if len(replicasets) != 1: + if replicasets and len(replicasets) != 1: # if more than one then sort by start time to newest is first replicasets.sort(key=lambda x: x['metadata']['creationTimestamp'], reverse=True) @@ -356,7 +356,8 @@ def _get_deploy_steps(self, batches, tags): # if there is no batch information available default to available nodes for app if not batches: # figure out how many nodes the application can go on - steps = len(self.node.get(labels=tags).json()['items']) + nodes = self.node.get(labels=tags).json()['items'] + steps = len(nodes) if nodes else 0 else: steps = int(batches) diff --git a/rootfs/scheduler/resources/pod.py b/rootfs/scheduler/resources/pod.py index e724373b5..04fa64359 100644 --- a/rootfs/scheduler/resources/pod.py +++ b/rootfs/scheduler/resources/pod.py @@ -485,10 +485,12 @@ def events(self, pod): 'involvedObject.namespace': pod['metadata']['namespace'], 'involvedObject.uid': pod['metadata']['uid'] } - events = self.ns.events(pod['metadata']['namespace'], fields=fields).json() + events = self.ns.events(pod['metadata']['namespace'], fields=fields).json()['items'] + if not events: + events = [] # make sure that events are sorted - events['items'].sort(key=lambda x: x['lastTimestamp']) - return events['items'] + events.sort(key=lambda x: x['lastTimestamp']) + return events def _handle_pod_errors(self, pod, reason, message): """ @@ -568,8 +570,10 @@ def _handle_pending_pods(self, namespace, labels): or throws errors as needed """ timeout = 0 - pods = self.get(namespace, labels=labels).json() - for pod in pods['items']: + pods = self.get(namespace, labels=labels).json()['items'] + if not pods: + pods = [] + for pod in pods: # only care about pods that are not starting or in the starting phases if pod['status']['phase'] not in ['Pending', 'ContainerCreating']: continue @@ -612,14 +616,16 @@ def wait_until_terminated(self, namespace, labels, current, desired): delta = current - desired self.log(namespace, "waiting for {} pods to be terminated ({}s timeout)".format(delta, timeout)) # noqa for waited in range(timeout): - pods = self.get(namespace, labels=labels).json() - count = len(pods['items']) + pods = self.get(namespace, labels=labels).json()['items'] + if not pods: + pods = [] + count = len(pods) # see if any pods are past their terminationGracePeriodsSeconds (as in stuck) # seems to be a problem in k8s around that: # https://github.com/kubernetes/kubernetes/search?q=terminating&type=Issues # these will be eventually GC'ed by k8s, ignoring them for now - for pod in pods['items']: + for pod in pods: # remove pod if it is passed the graceful termination period if self.deleted(pod): count -= 1 @@ -655,8 +661,10 @@ def wait_until_ready(self, namespace, containers, labels, desired, timeout): # self.log(namespace, 'Increasing timeout by {}s to allow a pull image operation to finish for pods'.format(additional_timeout)) # noqa count = 0 # ready pods - pods = self.get(namespace, labels=labels).json() - for pod in pods['items']: + pods = self.get(namespace, labels=labels).json()['items'] + if not pods: + pods = [] + for pod in pods: # now that state is running time to see if probes are passing if self.ready(pod): count += 1 @@ -691,8 +699,10 @@ def _handle_not_ready_pods(self, namespace, labels): Detects if any pod is in the Running phase but not Ready and handles any potential issues around that mainly failed healthcheks """ - pods = self.get(namespace, labels=labels).json() - for pod in pods['items']: + pods = self.get(namespace, labels=labels).json()['items'] + if not pods: + pods = [] + for pod in pods: # only care about pods that are in running phase if pod['status']['phase'] != 'Running': continue diff --git a/rootfs/scheduler/tests/test_deployments.py b/rootfs/scheduler/tests/test_deployments.py index 91f04490b..35582afeb 100644 --- a/rootfs/scheduler/tests/test_deployments.py +++ b/rootfs/scheduler/tests/test_deployments.py @@ -124,6 +124,12 @@ def test_delete(self): self.assertEqual(response.status_code, 200, data) def test_get_deployments(self): + # test when no deployments exist + response = self.scheduler.deployment.get(self.namespace) + data = response.json() + self.assertEqual(response.status_code, 200, data) + self.assertIn('items', data) + self.assertEqual(data['items'], None) # test success name = self.create() response = self.scheduler.deployment.get(self.namespace)