Repository: zalando-stups/senza

Issue:
Traffic command should continue when a stack is being turned down
If a stack is being deleted `senza traffic ...` fails with an error like:

<details>
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/dist-packages/senza/error_handling.py", line 66, in __call__
    self.function(*args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.5/dist-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.5/dist-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.5/dist-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/senza/cli.py", line 1073, in traffic
    print_version_traffic(ref, region)
  File "/usr/local/lib/python3.5/dist-packages/senza/traffic.py", line 292, in print_version_traffic
    versions = list(get_stack_versions(stack_ref.name, region))
  File "/usr/local/lib/python3.5/dist-packages/senza/traffic.py", line 228, in get_stack_versions
    lbs = elb.describe_load_balancers(LoadBalancerNames=[res.physical_resource_id])
  File "/usr/local/lib/python3.5/dist-packages/botocore/client.py", line 278, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.5/dist-packages/botocore/client.py", line 572, in _make_api_call
    raise ClientError(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (LoadBalancerNotFound) when calling the DescribeLoadBalancers operation: Cannot find Load Balancer xxx-XXXX
```
</details>

It should be able to continue and check the remaining stacks/elbs

Discussion:

Patch:
diff --git a/senza/manaus/utils.py b/senza/manaus/utils.py
new file mode 100644
index 0000000..97b8d55
--- /dev/null
+++ b/senza/manaus/utils.py
@@ -0,0 +1,15 @@
+from typing import Dict, Optional  # noqa: F401
+
+from botocore.exceptions import ClientError
+
+__all__ = ["extract_client_error_code"]
+
+
+def extract_client_error_code(exception: ClientError) -> Optional[str]:
+    """
+    Extracts the client error code from a boto ClientError exception. Returns
+    None if it fails.
+    """
+    error = exception.response.get('Error', {})  # type: Dict[str, Optional[str]]
+    error_code = error.get('Code')
+    return error_code
diff --git a/senza/traffic.py b/senza/traffic.py
index 60bc6b0..5eb8351 100644
--- a/senza/traffic.py
+++ b/senza/traffic.py
@@ -8,11 +8,13 @@ import dns.resolver
 from clickclick import Action, action, ok, print_table, warning
 
 from .aws import StackReference, get_stacks, get_tag
+from .manaus import ClientError
 from .manaus.boto_proxy import BotoClientProxy
 from .manaus.cloudformation import CloudFormationStack, ResourceType
 from .manaus.exceptions import ELBNotFound, StackNotFound, StackNotUpdated
 from .manaus.route53 import (RecordType, Route53, Route53HostedZone,
                              convert_cname_records_to_alias)
+from .manaus.utils import extract_client_error_code
 
 PERCENT_RESOLUTION = 2
 FULL_PERCENTAGE = PERCENT_RESOLUTION * 100
@@ -254,7 +256,12 @@ def get_stack_versions(stack_name: str, region: str) -> Iterator[StackVersion]:
         for res in details.resource_summaries.all():
             if res.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer':
                 elb = BotoClientProxy('elb', region)
-                lbs = elb.describe_load_balancers(LoadBalancerNames=[res.physical_resource_id])
+                try:
+                    lbs = elb.describe_load_balancers(LoadBalancerNames=[res.physical_resource_id])
+                except ClientError as e:
+                    error_code = extract_client_error_code(e)
+                    if error_code == 'LoadBalancerNotFound':
+                        continue
                 lb_dns_name.append(lbs['LoadBalancerDescriptions'][0]['DNSName'])
             elif res.resource_type == 'AWS::Route53::RecordSet':
                 if 'version' not in res.logical_id.lower():


Test patch:
diff --git a/tests/test_traffic.py b/tests/test_traffic.py
index 33a022d..0e3995b 100644
--- a/tests/test_traffic.py
+++ b/tests/test_traffic.py
@@ -1,7 +1,10 @@
 from unittest.mock import MagicMock
+
+import botocore.exceptions
 from senza.aws import SenzaStackSummary
-from senza.traffic import get_stack_versions, StackVersion, get_weights, resolve_to_ip_addresses
 from senza.manaus.route53 import RecordType
+from senza.traffic import (StackVersion, get_stack_versions, get_weights,
+                           resolve_to_ip_addresses)
 
 
 def test_get_stack_versions(monkeypatch):
@@ -38,7 +41,20 @@ def test_get_stack_versions(monkeypatch):
         return_value=[SenzaStackSummary(stack), SenzaStackSummary({'StackStatus': 'ROLLBACK_COMPLETE',
                                                                    'StackName': 'my-stack-1'})]))
     stack_version = list(get_stack_versions('my-stack', 'my-region'))
-    assert stack_version == [StackVersion('my-stack', '1', ['myapp.example.org'], ['elb-dns-name'], ['some-arn'])]
+    assert stack_version == [StackVersion('my-stack', '1',
+                                          ['myapp.example.org'],
+                                          ['elb-dns-name'],
+                                          ['some-arn'])]
+
+    elb.describe_load_balancers.side_effect = botocore.exceptions.ClientError(
+        {'Error': {'Code': 'LoadBalancerNotFound'}},
+        'foobar'
+    )
+    stack_version = list(get_stack_versions('my-stack', 'my-region'))
+    assert stack_version == [StackVersion('my-stack', '1',
+                                          ['myapp.example.org'],
+                                          [],
+                                          ['some-arn'])]
 
 
 def test_get_weights(monkeypatch):