diff --git a/docs/v3_spec.md b/docs/v3_spec.md index 8ddb6ceb..0278db86 100644 --- a/docs/v3_spec.md +++ b/docs/v3_spec.md @@ -152,21 +152,22 @@ ingress: paths: - path: / port: http + annotations: {} ``` -All applications will get a set of default hosts, if the cluster operator has defined ingress suffixes. +All applications will get a set of default hosts, if the cluster operator has defined ingress suffixes. If you do not specify a host in your `ingress` configuration, these default hosts will be used. -For example : +For example : 1. `your-app.example1.com` 2. `your-app.example2.com` -When you expose a path on a host you get that one as well. For example : +When you expose a path on a host you get that one as well. For example : ```yaml ingress: - host: example.com paths: - path: /my-path -``` +``` If you want to customize paths for default hosts as well, you can do it as : ```yaml @@ -179,8 +180,8 @@ ingress: ``` -This will make `/some-other-path` available on default hosts, but not on the host you provided in ingress. -Remember, default hosts will also contain the paths from the ingress. +This will make `/some-other-path` available on default hosts, but not on the host you provided in ingress. +Remember, default hosts will also contain the paths from the ingress. ### host @@ -203,7 +204,7 @@ ingress: - host: your-app.example.com ``` -If the operator of your cluster has configured host-rewrite rules they will be applied to the hostname given in this +If the operator of your cluster has configured host-rewrite rules they will be applied to the hostname given in this field. See [the operator guide](operator_guide.md#host-rewrite-rules) for details about how this feature works. In typical clusters, this value should be the host used by your application in production, and host-rewrite rules should @@ -234,6 +235,30 @@ your application. Requests to `your-app.example.com/metrics` will go to the port be defined under the `ports` configuration structure. It is also possible to use a port number, but named ports are strongly recommended. +### annotations + +| **Type** | **Required** | +|----------|--------------| +| object | no | + +A map of annotations to add to the ingress. + +Each entry in the ingress list that contains a non-empty `annotations` value will cause a separate Ingress object to +be created during the deployment. All items that have an empty value will have their hosts/paths merged into a single +Ingress. + +Example: +```yaml +ingress: + - host: your-app.example.com + paths: + - path: /foo + - host: other.example.com + paths: + - path: /foo + annotations: + some/annotation: bar +``` ## healthchecks diff --git a/fiaas_deploy_daemon/deployer/kubernetes/ingress.py b/fiaas_deploy_daemon/deployer/kubernetes/ingress.py index 6a15d5c6..c6d95704 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,88 @@ 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: + 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 +141,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 +157,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 +184,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 +204,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/setup.py b/setup.py index f69bd316..9795f24e 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def read(filename): "pinject == 0.14.1", "six == 1.12.0", "dnspython == 1.16.0", - "k8s == 0.14.0", + "k8s == 0.15.0", "monotonic == 1.5", "appdirs == 1.4.3", "requests-toolbelt == 0.9.1", diff --git a/tests/conftest.py b/tests/conftest.py index ae75b214..5906104e 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_any_call(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/e2e_expected/tls-ingress-multiple.yml b/tests/fiaas_deploy_daemon/e2e_expected/tls-ingress-multiple.yml index 236d60fa..e6b8dd93 100644 --- a/tests/fiaas_deploy_daemon/e2e_expected/tls-ingress-multiple.yml +++ b/tests/fiaas_deploy_daemon/e2e_expected/tls-ingress-multiple.yml @@ -22,9 +22,9 @@ spec: tls: - hosts: - qp4ouhml4krhfiltuenvy6ilm5pze3n3.svc.test.example.com - - v3-data-examples-tls-enabled-multiple.svc.test.example.com - example.com - example.org + - v3-data-examples-tls-enabled-multiple.svc.test.example.com secretName: v3-data-examples-tls-enabled-multiple-ingress-tls rules: - host: example.com 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():