From 8ed4bf1d2c17ae382acdfdd9e8bc2b3a36e9d9d4 Mon Sep 17 00:00:00 2001 From: Greg Jones Date: Fri, 8 May 2020 16:24:53 +0200 Subject: [PATCH] Support deploying multiple ingresses This adds support for deploying multiple ingresses with FIAAS, by separating the ingresses specified in the config by their distinct requirements for annotations. This is useful as it means for ingress controllers that use annotations for configuration - this can be determining which controller is used (public/private network, or configuration that affects behaviour (whitelisting, HTTP-auth) per host/path. --- .../deployer/kubernetes/ingress.py | 91 ++++++++++---- fiaas_deploy_daemon/specs/models.py | 1 + fiaas_deploy_daemon/specs/v3/defaults.yml | 1 + fiaas_deploy_daemon/specs/v3/factory.py | 6 +- tests/conftest.py | 4 +- tests/fiaas_deploy_daemon/conftest.py | 2 +- .../kubernetes/test_ingress_deploy.py | 111 ++++++++++++++---- .../e2e_expected/multiple_ingress1.yml | 50 ++++++++ .../e2e_expected/multiple_ingress2.yml | 44 +++++++ .../v3/data/examples/multiple_ingress.yml | 21 ++++ tests/fiaas_deploy_daemon/test_e2e.py | 60 ++++++++++ 11 files changed, 343 insertions(+), 48 deletions(-) create mode 100644 tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress1.yml create mode 100644 tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress2.yml create mode 100644 tests/fiaas_deploy_daemon/specs/v3/data/examples/multiple_ingress.yml diff --git a/fiaas_deploy_daemon/deployer/kubernetes/ingress.py b/fiaas_deploy_daemon/deployer/kubernetes/ingress.py index 6a15d5c6..f5318501 100644 --- a/fiaas_deploy_daemon/deployer/kubernetes/ingress.py +++ b/fiaas_deploy_daemon/deployer/kubernetes/ingress.py @@ -22,12 +22,14 @@ from itertools import chain from k8s.client import NotFound +from k8s.base import Equality, Inequality, Exists from k8s.models.common import ObjectMeta from k8s.models.ingress import Ingress, IngressSpec, IngressRule, HTTPIngressRuleValue, HTTPIngressPath, IngressBackend, \ IngressTLS from fiaas_deploy_daemon.retry import retry_on_upsert_conflict from fiaas_deploy_daemon.tools import merge_dicts +from collections import namedtuple LOG = logging.getLogger(__name__) @@ -43,48 +45,89 @@ def deploy(self, app_spec, labels): if self._should_have_ingress(app_spec): self._create(app_spec, labels) else: - self.delete(app_spec) + self._delete_unused(app_spec, labels) def delete(self, app_spec): - LOG.info("Deleting ingress for %s", app_spec.name) + LOG.info("Deleting ingresses for %s", app_spec.name) try: - Ingress.delete(app_spec.name, app_spec.namespace) + Ingress.delete_list(namespace=app_spec.namespace, labels={"app": Equality(app_spec.name), "fiaas/deployment_id": Exists()}) except NotFound: pass - @retry_on_upsert_conflict def _create(self, app_spec, labels): - LOG.info("Creating/updating ingress for %s", app_spec.name) - annotations = { - u"fiaas/expose": u"true" if _has_explicitly_set_host(app_spec) else u"false" + LOG.info("Creating/updating ingresses for %s", app_spec.name) + custom_labels = merge_dicts(app_spec.labels.ingress, labels) + + # Group app_spec.ingresses to separate those with annotations + AnnotatedIngress = namedtuple("AnnotatedIngress", ["name", "ingress_items", "annotations"]) + unannotated_ingress = AnnotatedIngress(name=app_spec.name, ingress_items=[], annotations={}) + ingresses_by_annotations = [unannotated_ingress] + for ingress_item in app_spec.ingresses: + LOG.info(ingress_item) + if ingress_item.annotations: + next_name = "{}-{}".format(app_spec.name, len(ingresses_by_annotations)) + annotated_ingresses = AnnotatedIngress(name=next_name, ingress_items=[ingress_item], annotations=ingress_item.annotations) + ingresses_by_annotations.append(annotated_ingresses) + else: + unannotated_ingress.ingress_items.append(ingress_item) + + LOG.info("Will create %s ingresses", len(ingresses_by_annotations)) + for annotated_ingress in ingresses_by_annotations: + if len(annotated_ingress.ingress_items) == 0: + LOG.info("No items, skipping: %s", annotated_ingress) + continue + + self._create_ingress(app_spec, annotated_ingress, custom_labels) + + self._delete_unused(app_spec, custom_labels) + + @retry_on_upsert_conflict + def _create_ingress(self, app_spec, annotated_ingress, labels): + default_annotations = { + u"fiaas/expose": u"true" if _has_explicitly_set_host(annotated_ingress.ingress_items) else u"false" } + annotations = merge_dicts(annotated_ingress.annotations, app_spec.annotations.ingress, default_annotations) - custom_labels = merge_dicts(app_spec.labels.ingress, labels) - custom_annotations = merge_dicts(app_spec.annotations.ingress, annotations) - metadata = ObjectMeta(name=app_spec.name, namespace=app_spec.namespace, labels=custom_labels, - annotations=custom_annotations) + metadata = ObjectMeta(name=annotated_ingress.name, namespace=app_spec.namespace, labels=labels, + annotations=annotations) per_host_ingress_rules = [ IngressRule(host=self._apply_host_rewrite_rules(ingress_item.host), http=self._make_http_ingress_rule_value(app_spec, ingress_item.pathmappings)) - for ingress_item in app_spec.ingresses + for ingress_item in annotated_ingress.ingress_items if ingress_item.host is not None ] - default_host_ingress_rules = self._create_default_host_ingress_rules(app_spec) + if annotated_ingress.annotations: + use_suffixes = False + host_ingress_rules = per_host_ingress_rules + else: + use_suffixes = True + host_ingress_rules = per_host_ingress_rules + self._create_default_host_ingress_rules(app_spec) - ingress_spec = IngressSpec(rules=per_host_ingress_rules + default_host_ingress_rules) + ingress_spec = IngressSpec(rules=host_ingress_rules) ingress = Ingress.get_or_create(metadata=metadata, spec=ingress_spec) - self._ingress_tls.apply(ingress, app_spec, self._get_hosts(app_spec)) + + hosts_for_tls = [rule.host for rule in host_ingress_rules] + self._ingress_tls.apply(ingress, app_spec, hosts_for_tls, use_suffixes=use_suffixes) self._owner_references.apply(ingress, app_spec) ingress.save() + def _delete_unused(self, app_spec, labels): + filter_labels = [ + ("app", Equality(labels["app"])), + ("fiaas/deployment_id", Exists()), + ("fiaas/deployment_id", Inequality(labels["fiaas/deployment_id"])) + ] + Ingress.delete_list(namespace=app_spec.namespace, labels=filter_labels) + def _generate_default_hosts(self, name): for suffix in self._ingress_suffixes: yield u"{}.{}".format(name, suffix) def _create_default_host_ingress_rules(self, app_spec): - all_pathmappings = chain.from_iterable(ingress_item.pathmappings for ingress_item in app_spec.ingresses) + all_pathmappings = chain.from_iterable(ingress_item.pathmappings + for ingress_item in app_spec.ingresses if not ingress_item.annotations) http_ingress_rule_value = self._make_http_ingress_rule_value(app_spec, all_pathmappings) return [IngressRule(host=host, http=http_ingress_rule_value) for host in self._generate_default_hosts(app_spec.name)] @@ -99,7 +142,7 @@ def _should_have_ingress(self, app_spec): return self._can_generate_host(app_spec) and _has_ingress(app_spec) and _has_http_port(app_spec) def _can_generate_host(self, app_spec): - return len(self._ingress_suffixes) > 0 or _has_explicitly_set_host(app_spec) + return len(self._ingress_suffixes) > 0 or _has_explicitly_set_host(app_spec.ingresses) @staticmethod def _make_http_ingress_rule_value(app_spec, pathmappings): @@ -115,8 +158,8 @@ def _get_hosts(self, app_spec): for ingress_item in app_spec.ingresses if ingress_item.host is not None] -def _has_explicitly_set_host(app_spec): - return any(ingress.host is not None for ingress in app_spec.ingresses) +def _has_explicitly_set_host(ingress_items): + return any(ingress_item.host is not None for ingress_item in ingress_items) def _has_http_port(app_spec): @@ -142,7 +185,7 @@ def __init__(self, config): self._shortest_suffix = sorted(config.ingress_suffixes, key=len)[0] if config.ingress_suffixes else None self.enable_deprecated_tls_entry_per_host = config.enable_deprecated_tls_entry_per_host - def apply(self, ingress, app_spec, hosts): + def apply(self, ingress, app_spec, hosts, use_suffixes=True): if self._should_have_ingress_tls(app_spec): tls_annotations = {} if self._cert_issuer or app_spec.ingress_tls.certificate_issuer: @@ -162,8 +205,12 @@ def apply(self, ingress, app_spec, hosts): else: ingress.spec.tls = [] - collapsed = self._collapse_hosts(app_spec, hosts) - ingress.spec.tls.append(IngressTLS(hosts=collapsed, secretName="{}-ingress-tls".format(app_spec.name))) + if use_suffixes: + # adding app-name to suffixes could result in a host too long to be the common-name of a cert, and + # as the user doesn't control it we should generate a host we know will fit + hosts = self._collapse_hosts(app_spec, hosts) + + ingress.spec.tls.append(IngressTLS(hosts=hosts, secretName="{}-ingress-tls".format(ingress.metadata.name))) def _collapse_hosts(self, app_spec, hosts): """The first hostname in the list will be used as Common Name in the certificate""" diff --git a/fiaas_deploy_daemon/specs/models.py b/fiaas_deploy_daemon/specs/models.py index 0ca6faf4..5923909a 100644 --- a/fiaas_deploy_daemon/specs/models.py +++ b/fiaas_deploy_daemon/specs/models.py @@ -119,6 +119,7 @@ def version(self): IngressItemSpec = namedtuple("IngressItemSpec", [ "host", "pathmappings", + "annotations" ]) IngressPathMappingSpec = namedtuple("IngressPathMappingSpec", [ diff --git a/fiaas_deploy_daemon/specs/v3/defaults.yml b/fiaas_deploy_daemon/specs/v3/defaults.yml index 8c108a51..fba00214 100644 --- a/fiaas_deploy_daemon/specs/v3/defaults.yml +++ b/fiaas_deploy_daemon/specs/v3/defaults.yml @@ -25,6 +25,7 @@ ingress: # Generate ingress rules for access from outside cluster. To disable, s paths: # List of paths exposed to which application port - path: / # Path the application answers on port: http # Name of the port path is served on + annotations: {} healthchecks: # Healthchecks defined for your application. If omitted and a single port is defined, liveness will default to http or tcp depending on port type, and readiness will be a copy of liveness. If no ports or multiple ports are defined, healthchecks are not provided and should be defined explicitly liveness: # Valid configuration requires exactly one of execute|http|tcp diff --git a/fiaas_deploy_daemon/specs/v3/factory.py b/fiaas_deploy_daemon/specs/v3/factory.py index 990b8d9d..2ce3e1a4 100644 --- a/fiaas_deploy_daemon/specs/v3/factory.py +++ b/fiaas_deploy_daemon/specs/v3/factory.py @@ -207,15 +207,15 @@ def resolve_port_number(port): else: raise InvalidConfiguration("{} is not a valid port name or port number".format(port)) - def ingress_item(host, paths): + def ingress_item(host, paths, annotations): ingress_path_mapping_specs = [ IngressPathMappingSpec(path=pathmapping["path"], port=resolve_port_number(pathmapping["port"])) for pathmapping in paths ] - return IngressItemSpec(host=host, pathmappings=ingress_path_mapping_specs) + return IngressItemSpec(host=host, pathmappings=ingress_path_mapping_specs, annotations=annotations) if len(http_ports.items()) > 0: - return [ingress_item(host_path_mapping["host"], host_path_mapping["paths"]) + return [ingress_item(host_path_mapping["host"], host_path_mapping["paths"], host_path_mapping["annotations"]) for host_path_mapping in ingress_lookup] else: return [] diff --git a/tests/conftest.py b/tests/conftest.py index ae75b214..e94afa0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,11 +36,11 @@ def prometheus_registry(): @pytest.helpers.register -def assert_any_call(mockk, first, *args): +def assert_any_call(mockk, first, *args, **kwargs): __tracebackhide__ = True def _assertion(): - mockk.assert_any_call(first, *args) + mockk.assert_called_once_with(first, *args, **kwargs) _add_useful_error_message(_assertion, mockk, first, args) diff --git a/tests/fiaas_deploy_daemon/conftest.py b/tests/fiaas_deploy_daemon/conftest.py index 440581f6..7b464ae1 100644 --- a/tests/fiaas_deploy_daemon/conftest.py +++ b/tests/fiaas_deploy_daemon/conftest.py @@ -62,7 +62,7 @@ def app_spec(): deployment_id="test_app_deployment_id", labels=LabelAndAnnotationSpec({}, {}, {}, {}, {}, {}), annotations=LabelAndAnnotationSpec({}, {}, {}, {}, {}, {}), - ingresses=[IngressItemSpec(host=None, pathmappings=[IngressPathMappingSpec(path="/", port=80)])], + ingresses=[IngressItemSpec(host=None, pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})], strongbox=StrongboxSpec(enabled=False, iam_role=None, aws_region="eu-west-1", groups=None), singleton=False, ingress_tls=IngressTlsSpec(enabled=False, certificate_issuer=None), diff --git a/tests/fiaas_deploy_daemon/deployer/kubernetes/test_ingress_deploy.py b/tests/fiaas_deploy_daemon/deployer/kubernetes/test_ingress_deploy.py index e3b694cf..7cafffad 100644 --- a/tests/fiaas_deploy_daemon/deployer/kubernetes/test_ingress_deploy.py +++ b/tests/fiaas_deploy_daemon/deployer/kubernetes/test_ingress_deploy.py @@ -30,7 +30,8 @@ from utils import TypeMatcher -LABELS = {"ingress_deployer": "pass through"} +LABELS = {"ingress_deployer": "pass through", "app": "testapp", "fiaas/deployment_id": "12345"} +LABEL_SELECTOR_PARAMS = {"labelSelector": "app=testapp,fiaas/deployment_id,fiaas/deployment_id!=12345"} INGRESSES_URI = '/apis/extensions/v1beta1/namespaces/default/ingresses/' @@ -62,7 +63,7 @@ def app_spec(**kwargs): deployment_id="test_app_deployment_id", labels=LabelAndAnnotationSpec({}, {}, {}, {}, {}, {}), annotations=LabelAndAnnotationSpec({}, {}, {}, {}, {}, {}), - ingresses=[IngressItemSpec(host=None, pathmappings=[IngressPathMappingSpec(path="/", port=80)])], + ingresses=[IngressItemSpec(host=None, pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})], strongbox=StrongboxSpec(enabled=False, iam_role=None, aws_region="eu-west-1", groups=None), singleton=False, ingress_tls=IngressTlsSpec(enabled=False, certificate_issuer=None), @@ -113,7 +114,7 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): ("only_default_hosts", app_spec(), ingress()), ("single_explicit_host", app_spec(ingresses=[ - IngressItemSpec(host="foo.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)])]), + IngressItemSpec(host="foo.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})]), ingress(expose=True, rules=[{ 'host': "foo.example.com", 'http': { @@ -152,7 +153,7 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): app_spec(ingresses=[ IngressItemSpec(host="foo.example.com", pathmappings=[ IngressPathMappingSpec(path="/", port=80), - IngressPathMappingSpec(path="/other", port=5000)])], + IngressPathMappingSpec(path="/other", port=5000)], annotations={})], ports=[ PortSpec(protocol="http", name="http", port=80, target_port=8080), PortSpec(protocol="http", name="other", port=5000, target_port=8081)]), @@ -210,8 +211,8 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): }])), ("multiple_explicit_hosts", app_spec(ingresses=[ - IngressItemSpec(host="foo.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)]), - IngressItemSpec(host="bar.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)])]), + IngressItemSpec(host="foo.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={}), + IngressItemSpec(host="bar.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})]), ingress(expose=True, rules=[{ 'host': "foo.example.com", 'http': { @@ -262,12 +263,11 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): IngressItemSpec(host="foo.example.com", pathmappings=[ IngressPathMappingSpec(path="/one", port=80), IngressPathMappingSpec(path="/two", port=5000) - ] - ), + ], annotations={}), IngressItemSpec(host="bar.example.com", pathmappings=[ IngressPathMappingSpec(path="/three", port=80), IngressPathMappingSpec(path="/four", port=5000) - ])], + ], annotations={})], ports=[ PortSpec(protocol="http", name="http", port=80, target_port=8080), PortSpec(protocol="http", name="other", port=5000, target_port=8081), @@ -367,7 +367,7 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): }])), ("rewrite_host_simple", app_spec(ingresses=[ - IngressItemSpec(host="rewrite.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)])]), + IngressItemSpec(host="rewrite.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})]), ingress(expose=True, rules=[{ 'host': "test.rewrite.example.com", 'http': { @@ -404,7 +404,7 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): }])), ("rewrite_host_regex_substitution", app_spec(ingresses=[ - IngressItemSpec(host="foo.rewrite.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)])]), + IngressItemSpec(host="foo.rewrite.example.com", pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})]), ingress(expose=True, rules=[{ 'host': "test.foo.rewrite.example.com", 'http': { @@ -446,14 +446,15 @@ def ingress(rules=None, metadata=None, expose=False, tls=None): annotations=LabelAndAnnotationSpec(deployment={}, horizontal_pod_autoscaler={}, ingress={"custom": "annotation"}, service={}, pod={}, status={})), ingress(metadata=pytest.helpers.create_metadata('testapp', external=False, - labels={"ingress_deployer": "pass through", "custom": "label"}, + labels={"ingress_deployer": "pass through", "custom": "label", + "app": "testapp", "fiaas/deployment_id": "12345"}, annotations={"fiaas/expose": "false", "custom": "annotation"}))), ("regex_path", app_spec(ingresses=[ IngressItemSpec(host=None, pathmappings=[ IngressPathMappingSpec( path=r"/(foo|bar/|other/(baz|quux)/stuff|foo.html|[1-5][0-9][0-9]$|[1-5][0-9][0-9]\..*$)", - port=80)])]), + port=80)], annotations={})]), ingress(expose=False, rules=[{ 'host': "testapp.svc.test.example.com", 'http': { @@ -513,7 +514,7 @@ def pytest_generate_tests(self, metafunc): metafunc.addcall(params, test_id) @pytest.mark.usefixtures("get") - def test_ingress_deploy(self, post, deployer, app_spec, expected_ingress, owner_references): + def test_ingress_deploy(self, post, delete, deployer, app_spec, expected_ingress, owner_references): mock_response = create_autospec(Response) mock_response.json.return_value = expected_ingress post.return_value = mock_response @@ -522,6 +523,75 @@ def test_ingress_deploy(self, post, deployer, app_spec, expected_ingress, owner_ pytest.helpers.assert_any_call(post, INGRESSES_URI, expected_ingress) owner_references.apply.assert_called_once_with(TypeMatcher(Ingress), app_spec) + delete.assert_called_once_with(INGRESSES_URI, body=None, params=LABEL_SELECTOR_PARAMS) + + @pytest.fixture + def dtparse(self): + with mock.patch('pyrfc3339.parse') as m: + yield m + + @pytest.mark.usefixtures("dtparse", "get") + def test_multiple_ingresses(self, post, delete, deployer, app_spec): + app_spec.ingresses.append(IngressItemSpec(host="extra.example.com", + pathmappings=[IngressPathMappingSpec(path="/", port=8000)], + annotations={"some/annotation": "some-value"})) + app_spec.ingresses.append(IngressItemSpec(host="extra.example.com", + pathmappings=[IngressPathMappingSpec(path="/_/ipblocked", port=8000)], + annotations={"some/allowlist": "10.0.0.1/12"})) + + expected_ingress = ingress() + mock_response = create_autospec(Response) + mock_response.json.return_value = expected_ingress + + expected_metadata2 = pytest.helpers.create_metadata('testapp-1', labels=LABELS, + annotations={"some/annotation": "some-value"}, external=True) + expected_ingress2 = ingress(rules=[ + { + "host": "extra.example.com", + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_spec.name, + "servicePort": 8000 + } + } + ] + } + } + ], metadata=expected_metadata2) + mock_response2 = create_autospec(Response) + mock_response.json.return_value = expected_ingress2 + + expected_metadata3 = pytest.helpers.create_metadata('testapp-2', labels=LABELS, + annotations={"some/allowlist": "10.0.0.1/12"}, external=True) + expected_ingress3 = ingress(rules=[ + { + "host": "extra.example.com", + "http": { + "paths": [ + { + "path": "/_/ipblocked", + "backend": { + "serviceName": app_spec.name, + "servicePort": 8000 + } + } + ] + } + } + ], metadata=expected_metadata3) + mock_response3 = create_autospec(Response) + mock_response3.json.return_value = expected_ingress3 + + post.side_effect = iter([mock_response, mock_response2, mock_response3]) + + deployer.deploy(app_spec, LABELS) + + post.assert_has_calls([mock.call(INGRESSES_URI, expected_ingress), mock.call(INGRESSES_URI, expected_ingress2), + mock.call(INGRESSES_URI, expected_ingress3)]) + delete.assert_called_once_with(INGRESSES_URI, body=None, params=LABEL_SELECTOR_PARAMS) @pytest.mark.parametrize("spec_name", ( "app_spec_thrift", @@ -533,27 +603,28 @@ def test_remove_existing_ingress_if_not_needed(self, request, delete, post, depl deployer.deploy(app_spec, LABELS) pytest.helpers.assert_no_calls(post, INGRESSES_URI) - pytest.helpers.assert_any_call(delete, INGRESSES_URI + "testapp") + pytest.helpers.assert_any_call(delete, INGRESSES_URI, body=None, params=LABEL_SELECTOR_PARAMS) @pytest.mark.usefixtures("get") def test_no_ingress(self, delete, post, deployer_no_suffix, app_spec): deployer_no_suffix.deploy(app_spec, LABELS) pytest.helpers.assert_no_calls(post, INGRESSES_URI) - pytest.helpers.assert_any_call(delete, INGRESSES_URI + "testapp") + pytest.helpers.assert_any_call(delete, INGRESSES_URI, body=None, params=LABEL_SELECTOR_PARAMS) @pytest.mark.parametrize("app_spec, hosts", ( (app_spec(), [u'testapp.svc.test.example.com', u'testapp.127.0.0.1.xip.io']), (app_spec(ingresses=[ IngressItemSpec(host="foo.rewrite.example.com", - pathmappings=[IngressPathMappingSpec(path="/", port=80)])]), - [u'testapp.svc.test.example.com', u'testapp.127.0.0.1.xip.io', u'test.foo.rewrite.example.com']), + pathmappings=[IngressPathMappingSpec(path="/", port=80)], annotations={})]), + [u'test.foo.rewrite.example.com', u'testapp.svc.test.example.com', u'testapp.127.0.0.1.xip.io']), )) + @pytest.mark.usefixtures("delete") def test_applies_ingress_tls(self, deployer, ingress_tls, app_spec, hosts): with mock.patch("k8s.models.ingress.Ingress.get_or_create") as get_or_create: get_or_create.return_value = mock.create_autospec(Ingress, spec_set=True) deployer.deploy(app_spec, LABELS) - ingress_tls.apply.assert_called_once_with(TypeMatcher(Ingress), app_spec, hosts) + ingress_tls.apply.assert_called_once_with(TypeMatcher(Ingress), app_spec, hosts, use_suffixes=True) class TestIngressTls(object): @@ -626,7 +697,7 @@ def tls(self, request, config): ], indirect=['tls']) def test_apply_tls(self, tls, app_spec, spec_tls, tls_annotations): ingress = Ingress() - ingress.metadata = ObjectMeta() + ingress.metadata = ObjectMeta(name=app_spec.name) ingress.spec = IngressSpec() tls.apply(ingress, app_spec, self.HOSTS) assert ingress.metadata.annotations == tls_annotations diff --git a/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress1.yml b/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress1.yml new file mode 100644 index 00000000..2e5064a0 --- /dev/null +++ b/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress1.yml @@ -0,0 +1,50 @@ + +# Copyright 2017-2019 The FIAAS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + fiaas/expose: "true" + labels: + app: v3-data-examples-multiple-ingress + fiaas/deployed_by: "" + fiaas/deployment_id: DEPLOYMENT_ID + fiaas/version: VERSION + name: v3-data-examples-multiple-ingress + namespace: default + ownerReferences: + - apiVersion: fiaas.schibsted.io/v1 + blockOwnerDeletion: true + controller: true + kind: Application + name: v3-data-examples-multiple-ingress + finalizers: [] +spec: + tls: [] + rules: + - host: www.example.com + http: + paths: + - backend: + serviceName: v3-data-examples-multiple-ingress + servicePort: '80' + path: / + - host: v3-data-examples-multiple-ingress.svc.test.example.com + http: + paths: + - backend: + serviceName: v3-data-examples-multiple-ingress + servicePort: '80' + path: / diff --git a/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress2.yml b/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress2.yml new file mode 100644 index 00000000..88d27153 --- /dev/null +++ b/tests/fiaas_deploy_daemon/e2e_expected/multiple_ingress2.yml @@ -0,0 +1,44 @@ + +# Copyright 2017-2019 The FIAAS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + fiaas/expose: "true" + foo/ingress-class: "internal" + labels: + app: v3-data-examples-multiple-ingress + fiaas/deployed_by: "" + fiaas/deployment_id: DEPLOYMENT_ID + fiaas/version: VERSION + name: v3-data-examples-multiple-ingress-1 + namespace: default + ownerReferences: + - apiVersion: fiaas.schibsted.io/v1 + blockOwnerDeletion: true + controller: true + kind: Application + name: v3-data-examples-multiple-ingress + finalizers: [] +spec: + tls: [] + rules: + - host: internal.example.com + http: + paths: + - backend: + serviceName: v3-data-examples-multiple-ingress + servicePort: '80' + path: / diff --git a/tests/fiaas_deploy_daemon/specs/v3/data/examples/multiple_ingress.yml b/tests/fiaas_deploy_daemon/specs/v3/data/examples/multiple_ingress.yml new file mode 100644 index 00000000..19a95bdb --- /dev/null +++ b/tests/fiaas_deploy_daemon/specs/v3/data/examples/multiple_ingress.yml @@ -0,0 +1,21 @@ + +# Copyright 2017-2019 The FIAAS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +version: 3 +ingress: + - host: www.example.com + - host: internal.example.com + annotations: + foo/ingress-class: internal diff --git a/tests/fiaas_deploy_daemon/test_e2e.py b/tests/fiaas_deploy_daemon/test_e2e.py index b9257dfb..f30c5150 100644 --- a/tests/fiaas_deploy_daemon/test_e2e.py +++ b/tests/fiaas_deploy_daemon/test_e2e.py @@ -328,6 +328,66 @@ def cleanup_complete(): wait_until(cleanup_complete, patience=PATIENCE) + @pytest.mark.usefixtures("fdd") + def test_multiple_ingresses(self, request, kind_logger): + with kind_logger(): + fiaas_path = "v3/data/examples/multiple_ingress.yml" + fiaas_yml = read_yml(request.fspath.dirpath().join("specs").join(fiaas_path).strpath) + + name = sanitize_resource_name(fiaas_path) + + expected = { + name: read_yml(request.fspath.dirpath().join("e2e_expected/multiple_ingress1.yml").strpath), + "{}-1".format(name): read_yml(request.fspath.dirpath().join("e2e_expected/multiple_ingress2.yml").strpath) + } + metadata = ObjectMeta(name=name, namespace="default", labels={"fiaas/deployment_id": DEPLOYMENT_ID1}) + spec = FiaasApplicationSpec(application=name, image=IMAGE1, config=fiaas_yml) + fiaas_application = FiaasApplication(metadata=metadata, spec=spec) + + fiaas_application.save() + app_uid = fiaas_application.metadata.uid + + # Check that deployment status is RUNNING + def _assert_status(): + status = FiaasApplicationStatus.get(create_name(name, DEPLOYMENT_ID1)) + assert status.result == u"RUNNING" + assert len(status.logs) > 0 + assert any("Saving result RUNNING for default/{}".format(name) in line for line in status.logs) + + wait_until(_assert_status, patience=PATIENCE) + + def _check_two_ingresses(): + assert Ingress.get(name) + assert Ingress.get("{}-1".format(name)) + + for ingress_name, expected_dict in expected.items(): + actual = Ingress.get(ingress_name) + assert_k8s_resource_matches(actual, expected_dict, IMAGE1, None, DEPLOYMENT_ID1, None, app_uid) + + wait_until(_check_two_ingresses, patience=PATIENCE) + + # Remove 2nd ingress to make sure cleanup works + fiaas_application.spec.config["ingress"].pop() + fiaas_application.metadata.labels["fiaas/deployment_id"] = DEPLOYMENT_ID2 + fiaas_application.save() + + def _check_one_ingress(): + assert Ingress.get(name) + with pytest.raises(NotFound): + Ingress.get("{}-1".format(name)) + + wait_until(_check_one_ingress, patience=PATIENCE) + + # Cleanup + FiaasApplication.delete(name) + + def cleanup_complete(): + for name, _ in expected.items(): + with pytest.raises(NotFound): + Ingress.get(name) + + wait_until(cleanup_complete, patience=PATIENCE) + def _deploy_success(name, kinds, service_type, image, expected, deployment_id, strongbox_groups=None, app_uid=None): def action():