Repository: zalando-stups/senza

Issue:
resolve_ssl_certificates for ACM: KeyError: 'CreatedAt'
```
cat /tmp/senza-traceback-28v30ylh
Traceback (most recent call last):
  File "/usr/lib/python3.5/site-packages/senza/error_handling.py", line 105, in __call__
    self.function(*args, **kwargs)
  File "/usr/lib/python3.5/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python3.5/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3.5/site-packages/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python3.5/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python3.5/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python3.5/site-packages/senza/cli.py", line 580, in create
    data = create_cf_template(definition, region, version, parameter, force, parameter_file)
  File "/usr/lib/python3.5/site-packages/senza/cli.py", line 669, in create_cf_template
    data = evaluate(definition.copy(), args, account_info, force)
  File "/usr/lib/python3.5/site-packages/senza/cli.py", line 237, in evaluate
    definition = componentfn(definition, configuration, args, info, force, account_info)
  File "/usr/lib/python3.5/site-packages/senza/components/weighted_dns_elastic_load_balancer.py", line 49, in component_weighted_dns_elastic_load_balancer
    account_info)
  File "/usr/lib/python3.5/site-packages/senza/components/elastic_load_balancer.py", line 135, in component_elastic_load_balancer
    listeners = resolve_ssl_certificates(listeners, subdomain, main_zone, account_info)
  File "/usr/lib/python3.5/site-packages/senza/components/elastic_load_balancer.py", line 95, in resolve_ssl_certificates
    ssl_cert = get_ssl_cert(subdomain, main_zone, listener, account_info)
  File "/usr/lib/python3.5/site-packages/senza/components/elastic_load_balancer.py", line 52, in get_ssl_cert
    reverse=True)
  File "/usr/lib/python3.5/site-packages/senza/manaus/acm.py", line 178, in get_certificates
    certificate = ACMCertificate.get_by_arn(self.region, arn)
  File "/usr/lib/python3.5/site-packages/senza/manaus/acm.py", line 110, in get_by_arn
    return cls.from_boto_dict(certificate)
  File "/usr/lib/python3.5/site-packages/senza/manaus/acm.py", line 84, in from_boto_dict
    created_at = certificate['CreatedAt']
KeyError: 'CreatedAt'
```

Imported certs don't have `CreatedAt`:
```
>>> c.describe_certificate(CertificateArn='arn:aws:acm:eu-central-1:123456789012:certificate/f14f9718-7da8-4250-9c21-d0341da4e44f')
{'Certificate': {'NotAfter': datetime.datetime(2017, 5, 7, 10, 0, tzinfo=tzlocal()), 'SubjectAlternativeNames': ['pierone.stups.zalan.do', 'registry.opensource.zalan.do'], 'Subject': 'C=DE,L=Berlin,O=Zalando SE,OU=CDP,CN=CDP Proxy', 'Serial': '2a:d2:f6:d3:23:62:0b:5f:f8:2f:d6:3a:4a:9f:b8:c6:48:a0:11:62', 'Type': 'IMPORTED', 'ImportedAt': datetime.datetime(2017, 4, 7, 10, 12, 55, tzinfo=tzlocal()), 'SignatureAlgorithm': 'SHA256WITHRSA', 'CertificateArn': 'arn:aws:acm:eu-central-1:123456789012:certificate/f14f9718-7da8-4250-9c21-d0341da4e44f', 'NotBefore': datetime.datetime(2017, 4, 7, 10, 0, tzinfo=tzlocal()), 'DomainValidationOptions': [{'DomainName': 'pierone.stups.zalan.do'}, {'DomainName': 'registry.opensource.zalan.do'}], 'Status': 'ISSUED', 'DomainName': 'pierone.stups.zalan.do', 'Issuer': 'Zalando SE', 'InUseBy': [], 'KeyAlgorithm': 'RSA-2048'}, 'ResponseMetadata': {'HTTPHeaders': {'content-type': 'application/x-amz-json-1.1', 'x-amzn-requestid': '514a08ad-1b71-11e7-91d6-c5bca103e8f9', 'date': 'Fri, 07 Apr 2017 09:05:16 GMT', 'content-length': '695'}, 'RequestId': '514a08ad-1b71-11e7-91d6-c5bca103e8f9', 'HTTPStatusCode': 200, 'RetryAttempts': 0}}
```

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/10978001/badge)](https://coveralls.io/builds/10978001)

Changes Unknown when pulling **81412ebca59282719b16c1387624a0d97cbc0348 on acm-createdat-key-error** into ** on master**.

hjacobs: :+1: 

Patch:
diff --git a/senza/manaus/acm.py b/senza/manaus/acm.py
index 8d85313..f43ca8c 100644
--- a/senza/manaus/acm.py
+++ b/senza/manaus/acm.py
@@ -81,7 +81,7 @@ class ACMCertificate:
         subject_alternative_name = certificate['SubjectAlternativeNames']
         domain_validation_options = certificate['DomainValidationOptions']
         subject = certificate['Subject']
-        created_at = certificate['CreatedAt']
+        created_at = certificate.get('CreatedAt')
         status = certificate['Status']
         signature_algorithm = certificate['SignatureAlgorithm']
         in_use_by = certificate['InUseBy']


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 2d91e42..ae21b24 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -20,7 +20,7 @@ CERT1_ZO_NE = {'CertificateArn': 'arn:aws:acm:eu-west-1:cert1',
                'IssuedAt': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                'Issuer': 'SenzaTest',
                'KeyAlgorithm': 'RSA-2048',
-               'NotAfter': datetime(2017, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
+               'NotAfter': datetime(2020, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                'NotBefore': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                'Serial': '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00',
                'SignatureAlgorithm': 'SHA256WITHRSA',
@@ -45,7 +45,7 @@ CERT1_ZO_NE_REVOKED = {'CertificateArn': 'arn:aws:acm:eu-west-1:cert1',
                        'IssuedAt': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                        'Issuer': 'SenzaTest',
                        'KeyAlgorithm': 'RSA-2048',
-                       'NotAfter': datetime(2017, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
+                       'NotAfter': datetime(2020, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                        'NotBefore': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
                        'Serial': '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00',
                        'SignatureAlgorithm': 'SHA256WITHRSA',
@@ -86,7 +86,7 @@ HOSTED_ZONE_ZO_NE_DEV = {'Config': {'PrivateZone': False},
 SERVER_CERT_ZO_NE = MagicMock(name='zo-ne')
 SERVER_CERT_ZO_NE.server_certificate_metadata = {'Arn': 'arn:aws:123',
                                                  'ServerCertificateName': 'zo-ne',
-                                                 'Expiration': datetime(2017, 4, 1, 12, 14, 14,
+                                                 'Expiration': datetime(2020, 4, 1, 12, 14, 14,
                                                                         tzinfo=timezone(timedelta(hours=2))),
                                                  'Path': '/',
                                                  'ServerCertificateId': '000',
diff --git a/tests/test_manaus/test_acm.py b/tests/test_manaus/test_acm.py
index f022ff2..a10013b 100644
--- a/tests/test_manaus/test_acm.py
+++ b/tests/test_manaus/test_acm.py
@@ -34,7 +34,7 @@ CERT1 = {'CertificateArn': 'arn:aws:acm:eu-west-1:cert1',
          'IssuedAt': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
          'Issuer': 'SenzaTest',
          'KeyAlgorithm': 'RSA-2048',
-         'NotAfter': datetime(2017, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
+         'NotAfter': datetime(2020, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
          'NotBefore': datetime(2016, 4, 1, 12, 14, 14, tzinfo=timezone.utc),
          'Serial': '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00',
          'SignatureAlgorithm': 'SHA256WITHRSA',
@@ -75,7 +75,7 @@ CERT2 = {'CertificateArn': 'arn:aws:acm:eu-west-1:cert2',
          'IssuedAt': datetime(2016, 4, 1, 12, 14, 14),
          'Issuer': 'SenzaTest',
          'KeyAlgorithm': 'RSA-2048',
-         'NotAfter': datetime(2017, 4, 1, 12, 14, 14),
+         'NotAfter': datetime(2020, 4, 1, 12, 14, 14),
          'NotBefore': datetime(2016, 4, 1, 12, 14, 14),
          'Serial': '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00',
          'SignatureAlgorithm': 'SHA256WITHRSA',
@@ -85,6 +85,23 @@ CERT2 = {'CertificateArn': 'arn:aws:acm:eu-west-1:cert2',
                                      '*.senza.aws.example.net',
                                      '*.app.example.net']}
 
+CERT3 = {
+        'NotAfter': datetime(2017, 5, 7, 10, 0, tzinfo=timezone.utc),
+        'SubjectAlternativeNames': ['pierone.stups.zalan.do', 'registry.opensource.zalan.do'],
+        'Subject': 'C=DE,L=Berlin,O=Zalando SE,OU=CDP,CN=CDP Proxy',
+        'Serial': '2a:d2:f6:d3:23:62:0b:5f:f8:2f:d6:3a:4a:9f:b8:c6:48:a0:11:62',
+        'Type': 'IMPORTED',
+        'ImportedAt': datetime(2017, 4, 7, 10, 12, 55, tzinfo=timezone.utc),
+        'SignatureAlgorithm': 'SHA256WITHRSA',
+        'CertificateArn': 'arn:aws:acm:eu-west-1:cert3',
+        'NotBefore': datetime(2017, 4, 7, 10, 0, tzinfo=timezone.utc),
+        'DomainValidationOptions': [{'DomainName': 'pierone.stups.zalan.do'}, {'DomainName': 'registry.opensource.zalan.do'}],
+        'Status': 'ISSUED',
+        'DomainName': 'pierone.stups.zalan.do',
+        'Issuer': 'Zalando SE',
+        'InUseBy': [],
+        'KeyAlgorithm': 'RSA-2048'}
+
 CERT_VALIDATION_TIMED_OUT = {
     'KeyAlgorithm': 'RSA-2048',
     'DomainName': 'alpha.example.org',
@@ -109,7 +126,7 @@ def test_certificate_valid():
     assert certificate1.domain_name == '*.senza.example.com'
     assert certificate1.is_valid(when=datetime(2016, 4, 5, 12, 14, 14,
                                                tzinfo=timezone.utc))
-    assert not certificate1.is_valid(when=datetime(2018, 4, 5, 12, 14, 14,
+    assert not certificate1.is_valid(when=datetime(2021, 4, 5, 12, 14, 14,
                                                    tzinfo=timezone.utc))
     assert not certificate1.is_valid(when=datetime(2013, 4, 2, 10, 11, 12,
                                                    tzinfo=timezone.utc))
@@ -121,7 +138,7 @@ def test_certificate_valid():
     assert certificate1_revoked.domain_name == '*.senza.example.com'
     assert not certificate1_revoked.is_valid(when=datetime(2016, 4, 5, 12, 14, 14,
                                                            tzinfo=timezone.utc))
-    assert not certificate1_revoked.is_valid(when=datetime(2018, 4, 5, 12, 14, 14,
+    assert not certificate1_revoked.is_valid(when=datetime(2021, 4, 5, 12, 14, 14,
                                                            tzinfo=timezone.utc))
     assert not certificate1_revoked.is_valid(when=datetime(2013, 4, 2, 10, 11, 12,
                                                            tzinfo=timezone.utc))
@@ -153,7 +170,7 @@ def test_certificate_get_by_arn(monkeypatch):
     assert certificate1.domain_name == '*.senza.example.com'
     assert certificate1.is_valid(when=datetime(2016, 4, 5, 12, 14, 14,
                                                tzinfo=timezone.utc))
-    assert not certificate1.is_valid(when=datetime(2018, 4, 5, 12, 14, 14,
+    assert not certificate1.is_valid(when=datetime(2021, 4, 5, 12, 14, 14,
                                                    tzinfo=timezone.utc))
     assert not certificate1.is_valid(when=datetime(2013, 4, 2, 10, 11, 12,
                                                    tzinfo=timezone.utc))
@@ -201,6 +218,12 @@ def test_get_certificates(monkeypatch):
     assert len(certificates_net) == 1
     assert certificates_net[0].arn == 'arn:aws:acm:eu-west-1:cert2'
 
+    m_client.describe_certificate.side_effect = [{'Certificate': CERT3}]
+    certificates_net = list(acm.get_certificates(valid_only=False,
+                                                 domain_name="registry.opensource.zalan.do"))
+    assert len(certificates_net) == 1
+    assert certificates_net[0].arn == 'arn:aws:acm:eu-west-1:cert3'
+
 
 def test_arn_is_acm_certificate():
     assert ACMCertificate.arn_is_acm_certificate('arn:aws:acm:certificate')