Repository: zalando-stups/senza

Issue:
Make property overwriting for senza components less destructive
When using a senza component to build a stack, it would be great if it was possible to overwrite properties in a less destructive way. 

### Expected Behavior
We are using the default component `WeightedDnsElasticLoadBalancer`. Now we want to open port 80 in addition to port 443 in order to allow our application to redirect users from http to https. We want to keep all of the configuration and extend it with one additional entry.

### Actual Behavior
Currently this is not possible, because we can only use the option to overwrite the `Listeners` property like this:
```
  - AppLoadBalancer:
      Type: Senza::WeightedDnsElasticLoadBalancer
      ...
      Listeners:
      - LoadBalancerPort: 80
        Protocol: HTTP
        InstancePort: 80
        InstanceProtocol: HTTP
      - LoadBalancerPort: 443
        Protocol: HTTPS
        InstancePort: 80
        InstanceProtocol: HTTP
        SSLCertificateId: "????"
```
Doing this, we completely miss out on the ease of use that the component provides with regard to selecting the correct SSL certificate, because our definition completely overwrites the one senza creates.

--

As a solution it would be nice if we could either flag our `Listeners` entry as "these are additional, not a replacement" or alternatively have a way of tell senza to inject the SSLCertificateId.

Discussion:

Patch:
diff --git a/senza/components/elastic_load_balancer.py b/senza/components/elastic_load_balancer.py
index 1a0fdb4..68972bd 100644
--- a/senza/components/elastic_load_balancer.py
+++ b/senza/components/elastic_load_balancer.py
@@ -76,13 +76,11 @@ def get_ssl_cert(subdomain, main_zone, configuration, account_info: AccountArgum
     return ssl_cert
 
 
-def get_listeners(subdomain, main_zone, configuration,
-                  account_info: AccountArguments):
-    ssl_cert = get_ssl_cert(subdomain, main_zone, configuration, account_info)
+def get_listeners(configuration):
     return [
         {
             "PolicyNames": [],
-            "SSLCertificateId": ssl_cert,
+            "SSLCertificateId": configuration.get('SSLCertificateId'),
             "Protocol": "HTTPS",
             "InstancePort": configuration["HTTPPort"],
             "LoadBalancerPort": 443
@@ -90,6 +88,16 @@ def get_listeners(subdomain, main_zone, configuration,
     ]
 
 
+def resolve_ssl_certificates(listeners, subdomain, main_zone, account_info):
+    new_listeners = []
+    for listener in listeners:
+        if listener.get('Protocol') in ('HTTPS', 'SSL'):
+            ssl_cert = get_ssl_cert(subdomain, main_zone, listener, account_info)
+            listener['SSLCertificateId'] = ssl_cert
+        new_listeners.append(listener)
+    return new_listeners
+
+
 def component_elastic_load_balancer(definition,
                                     configuration: dict,
                                     args: TemplateArguments,
@@ -123,7 +131,8 @@ def component_elastic_load_balancer(definition,
             subdomain = domain['Subdomain']
             main_zone = domain['Zone']  # type: str
 
-    listeners = configuration.get('Listeners') or get_listeners(subdomain, main_zone, configuration, account_info)
+    listeners = configuration.get('Listeners') or get_listeners(configuration)
+    listeners = resolve_ssl_certificates(listeners, subdomain, main_zone, account_info)
 
     health_check_protocol = configuration.get('HealthCheckProtocol') or 'HTTP'
 


Test patch:
diff --git a/tests/test_components.py b/tests/test_components.py
index dfbf912..a7fe6f0 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -198,6 +198,31 @@ def test_component_load_balancer_http_only(monkeypatch):
     assert 'Bar' == result["Resources"]["test_lb"]["Properties"]["Listeners"][0]["Foo"]
 
 
+def test_component_load_balancer_listeners_ssl(monkeypatch):
+    configuration = {
+        "Name": "test_lb",
+        "SecurityGroups": "",
+        "HTTPPort": "9999",
+        "Listeners": [{"Protocol": "SSL"}]
+    }
+    info = {'StackName': 'foobar', 'StackVersion': '0.1'}
+    definition = {"Resources": {}}
+
+    args = MagicMock()
+    args.region = "foo"
+
+    mock_string_result = MagicMock()
+    mock_string_result.return_value = "foo"
+    monkeypatch.setattr('senza.components.elastic_load_balancer.resolve_security_groups', mock_string_result)
+
+    get_ssl_cert = MagicMock()
+    get_ssl_cert.return_value = 'my-ssl-arn'
+    monkeypatch.setattr('senza.components.elastic_load_balancer.get_ssl_cert', get_ssl_cert)
+
+    result = component_elastic_load_balancer(definition, configuration, args, info, False, MagicMock())
+    assert 'my-ssl-arn' == result["Resources"]["test_lb"]["Properties"]["Listeners"][0]["SSLCertificateId"]
+
+
 def test_component_load_balancer_namelength(monkeypatch):
     configuration = {
         "Name": "test_lb",