Repository: zalando-stups/senza

Issue:
NoRegionError: You must specify a region.
When trying to run `senza create` I am getting this error:

```
$ senza create --region eu-central-1 --force ../hello-flask/hello.yaml v56 ImageVersion=bus56 --stacktrace-visible
Generating Cloud Formation template.. EXCEPTION OCCURRED: You must specify a region.
Traceback (most recent call last):
  File "/Users/rcaricio/.virtualenvs/lizzy-init/bin/senza", line 11, in <module>
    sys.exit(main())
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/cli.py", line 1492, in main
    HandleExceptions(cli)()
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/error_handling.py", line 99, in __call__
    self.die_unknown_error(e)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/error_handling.py", line 57, in die_unknown_error
    raise e
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/error_handling.py", line 69, in __call__
    self.function(*args, **kwargs)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/cli.py", line 555, in create
    data = create_cf_template(definition, region, version, parameter, force, parameter_file)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/cli.py", line 638, in create_cf_template
    data = evaluate(definition.copy(), args, account_info, force)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/cli.py", line 239, in evaluate
    definition = componentfn(definition, configuration, args, info, force, account_info)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/components/weighted_dns_elastic_load_balancer.py", line 29, in component_weighted_dns_elastic_load_balancer
    return component_elastic_load_balancer(definition, configuration, args, info, force, account_info)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/components/elastic_load_balancer.py", line 110, in component_elastic_load_balancer
    listeners = configuration.get('Listeners') or get_listeners(subdomain, main_zone, configuration)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/components/elastic_load_balancer.py", line 48, in get_listeners
    reverse=True)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/senza/manaus/acm.py", line 169, in get_certificates
    client = boto3.client('acm')
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/boto3/__init__.py", line 79, in client
    return _get_default_session().client(*args, **kwargs)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/boto3/session.py", line 250, in client
    aws_session_token=aws_session_token, config=config)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/session.py", line 818, in create_client
    client_config=config, api_version=api_version)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/client.py", line 69, in create_client
    verify, credentials, scoped_config, client_config, endpoint_bridge)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/client.py", line 199, in _get_client_args
    service_name, region_name, endpoint_url, is_secure)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/client.py", line 322, in resolve
    service_name, region_name)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/regions.py", line 122, in construct_endpoint
    partition, service_name, region_name)
  File "/Users/rcaricio/.virtualenvs/lizzy-init/lib/python3.5/site-packages/botocore/regions.py", line 135, in _endpoint_for_partition
    raise NoRegionError()
botocore.exceptions.NoRegionError: You must specify a region.
```

Senza version `1.0.91`

Does not happen with older versions of Senza though.


Discussion:

Patch:
diff --git a/senza/components/elastic_load_balancer.py b/senza/components/elastic_load_balancer.py
index 347b515..5644c13 100644
--- a/senza/components/elastic_load_balancer.py
+++ b/senza/components/elastic_load_balancer.py
@@ -2,6 +2,7 @@ import click
 from clickclick import fatal_error
 
 from senza.aws import resolve_security_groups
+from ..cli import AccountArguments, TemplateArguments
 from ..manaus import ClientError
 from ..manaus.iam import IAM, IAMServerCertificate
 from ..manaus.acm import ACM, ACMCertificate
@@ -23,13 +24,14 @@ def get_load_balancer_name(stack_name: str, stack_version: str):
     return '{}-{}'.format(stack_name[:l], stack_version)
 
 
-def get_listeners(subdomain, main_zone, configuration):
+def get_listeners(subdomain, main_zone, configuration,
+                  account_info: AccountArguments):
     ssl_cert = configuration.get('SSLCertificateId')
 
     if ACMCertificate.arn_is_acm_certificate(ssl_cert):
         # check if certificate really exists
         try:
-            ACMCertificate.get_by_arn(ssl_cert)
+            ACMCertificate.get_by_arn(account_info.Region, ssl_cert)
         except ClientError as e:
             error_msg = e.response['Error']['Message']
             fatal_error(error_msg)
@@ -44,7 +46,8 @@ def get_listeners(subdomain, main_zone, configuration):
             iam_pattern = main_zone.lower().rstrip('.').replace('.', '-')
             name = '{sub}.{zone}'.format(sub=subdomain,
                                          zone=main_zone.rstrip('.'))
-            acm_certificates = sorted(ACM.get_certificates(domain_name=name),
+            acm = ACM(account_info.Region)
+            acm_certificates = sorted(acm.get_certificates(domain_name=name),
                                       reverse=True)
         else:
             iam_pattern = ''
@@ -79,9 +82,13 @@ def get_listeners(subdomain, main_zone, configuration):
     ]
 
 
-def component_elastic_load_balancer(definition, configuration, args, info, force, account_info):
+def component_elastic_load_balancer(definition,
+                                    configuration: dict,
+                                    args: TemplateArguments,
+                                    info: dict,
+                                    force,
+                                    account_info: AccountArguments):
     lb_name = configuration["Name"]
-
     # domains pointing to the load balancer
     subdomain = ''
     main_zone = None
@@ -107,7 +114,7 @@ def component_elastic_load_balancer(definition, configuration, args, info, force
             subdomain = domain['Subdomain']
             main_zone = domain['Zone']  # type: str
 
-    listeners = configuration.get('Listeners') or get_listeners(subdomain, main_zone, configuration)
+    listeners = configuration.get('Listeners') or get_listeners(subdomain, main_zone, configuration, account_info)
 
     health_check_protocol = "HTTP"
     allowed_health_check_protocols = ("HTTP", "TCP", "UDP", "SSL")
diff --git a/senza/manaus/acm.py b/senza/manaus/acm.py
index ad918e8..d28fe26 100644
--- a/senza/manaus/acm.py
+++ b/senza/manaus/acm.py
@@ -101,11 +101,11 @@ class ACMCertificate:
                    revoked_at, revocation_reason)
 
     @classmethod
-    def get_by_arn(cls, arn: str) -> "ACMCertificate":
+    def get_by_arn(cls, region: str, arn: str) -> "ACMCertificate":
         """
         Gets a ACMCertificate based on ARN alone
         """
-        client = boto3.client('acm')
+        client = boto3.client('acm', region)
         certificate = client.describe_certificate(CertificateArn=arn)['Certificate']
         return cls.from_boto_dict(certificate)
 
@@ -156,21 +156,26 @@ class ACM:
     See http://boto3.readthedocs.io/en/latest/reference/services/acm.html
     """
 
-    @staticmethod
-    def get_certificates(valid_only: bool=True,
+    def __init__(self, region=str):
+        self.region = region
+
+    def get_certificates(self,
+                         *,
+                         valid_only: bool=True,
                          domain_name: Optional[str]=None) -> Iterator[ACMCertificate]:
         """
         Gets certificates from ACM. By default it returns all valid certificates
 
+        :param region: AWS region
         :param valid_only: Return only valid certificates
         :param domain_name: Return only certificates that match the domain
         """
         # TODO implement pagination
-        client = boto3.client('acm')
+        client = boto3.client('acm', self.region)
         certificates = client.list_certificates()['CertificateSummaryList']
         for summary in certificates:
             arn = summary['CertificateArn']
-            certificate = ACMCertificate.get_by_arn(arn)
+            certificate = ACMCertificate.get_by_arn(self.region, arn)
             if valid_only and not certificate.is_valid():
                 pass
             elif domain_name is not None and not certificate.matches(domain_name):


Test patch:
diff --git a/tests/test_manaus/test_acm.py b/tests/test_manaus/test_acm.py
index 51e12d4..f022ff2 100644
--- a/tests/test_manaus/test_acm.py
+++ b/tests/test_manaus/test_acm.py
@@ -148,7 +148,8 @@ def test_certificate_get_by_arn(monkeypatch):
     m_client.describe_certificate.return_value = {'Certificate': CERT1}
     monkeypatch.setattr('boto3.client', m_client)
 
-    certificate1 = ACMCertificate.get_by_arn('arn:aws:acm:eu-west-1:cert')
+    certificate1 = ACMCertificate.get_by_arn('dummy-region',
+                                             'arn:aws:acm:eu-west-1:cert')
     assert certificate1.domain_name == '*.senza.example.com'
     assert certificate1.is_valid(when=datetime(2016, 4, 5, 12, 14, 14,
                                                tzinfo=timezone.utc))
@@ -183,7 +184,7 @@ def test_get_certificates(monkeypatch):
                                            tzinfo=timezone.utc)
     monkeypatch.setattr('senza.manaus.acm.datetime', m_datetime)
 
-    acm = ACM()
+    acm = ACM('dummy-region')
     certificates_default = list(acm.get_certificates())
     assert len(certificates_default) == 1  # Cert2 is excluded because it's REVOKED
     assert certificates_default[0].arn == 'arn:aws:acm:eu-west-1:cert1'