diff --git a/newsfragments/1113.internal.md b/newsfragments/1113.internal.md new file mode 100644 index 000000000..1197476cb --- /dev/null +++ b/newsfragments/1113.internal.md @@ -0,0 +1 @@ +CI: Document manifests tests with docstring and better assertion messages. diff --git a/tests/manifests/test_ingresses.py b/tests/manifests/test_ingresses.py index 69be271a3..3d72d9b2e 100644 --- a/tests/manifests/test_ingresses.py +++ b/tests/manifests/test_ingresses.py @@ -21,6 +21,13 @@ @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_has_ingress(templates): + """ + Ingress resources must be generated for deployables that support ingress. + + When a deployable has ingress support (has_ingress=True), the Helm chart must + generate corresponding Ingress resources. This ensures all ingress-capable + components are properly exposed via Kubernetes Ingress. + """ seen_deployables = set[DeployableDetails]() seen_deployables_with_ingresses = set[DeployableDetails]() @@ -31,7 +38,9 @@ async def test_has_ingress(templates): seen_deployables_with_ingresses.add(deployable_details) for seen_deployable in seen_deployables_with_ingresses: - assert seen_deployable.has_ingress + assert seen_deployable.has_ingress, ( + f"Deployable {seen_deployable.name} has Ingress resource but has_ingress is False" + ) @pytest.mark.parametrize( @@ -46,6 +55,17 @@ async def test_has_ingress(templates): ) @pytest.mark.asyncio_cooperative async def test_ingress_is_expected_host(values, templates): + """ + Ingress resources must use the expected hostnames from Helm values. + + When Helm values configure specific hostnames for ingress resources: + - Ingress rules must use the exact hostnames specified in values + - For well-known deployables, use serverName if no host is specified + - All configured hosts must be present in generated Ingress resources + + This ensures proper DNS routing and hostname-based virtual hosting. + """ + def get_hosts_from_fragment(values_fragment, deployable_details): if deployable_details.name == "well-known": if not values_fragment.get("host"): @@ -69,44 +89,81 @@ def get_hosts(): found_hosts = [] for template in templates: if template["kind"] == "Ingress": - assert "rules" in template["spec"] - assert len(template["spec"]["rules"]) > 0 + assert "rules" in template["spec"], f"{template_id(template)} is missing required 'rules' specification" + assert len(template["spec"]["rules"]) > 0, ( + f"{template_id(template)} has no rules defined. Ingress must have at least one rule." + ) for rule in template["spec"]["rules"]: - assert "host" in rule + assert "host" in rule, f"{template_id(template)} rule is missing required 'host' field: {rule}" found_hosts.append(rule["host"]) - assert set(found_hosts) == set(expected_hosts) + assert set(found_hosts) == set(expected_hosts), ( + f"Host mismatch: Expected {sorted(set(expected_hosts))}, found {sorted(set(found_hosts))}" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_ingress_paths_are_all_prefix(templates): + """ + Ingress paths must use Prefix pathType for proper path-based routing. + + All HTTP paths in Ingress resources must use pathType: Prefix to ensure + consistent path matching behavior across different ingress controllers. + ImplementationSpecific pathType is unacceptable as we want to be ingress-controller agnostic + and only use Kubernetes-standard options. + + This ensures reliable path-based routing and compatibility with various ingress controllers. + """ for template in templates: if template["kind"] == "Ingress": - assert "rules" in template["spec"] - assert len(template["spec"]["rules"]) > 0 + assert "rules" in template["spec"], f"{template_id(template)} is missing required 'rules' specification" + assert len(template["spec"]["rules"]) > 0, ( + f"{template_id(template)} has no rules defined. Ingress must have at least one rule." + ) for rule in template["spec"]["rules"]: - assert "http" in rule - assert "paths" in rule["http"] + assert "http" in rule, f"{template_id(template)} rule is missing required 'http' specification: {rule}" + assert "paths" in rule["http"], ( + f"{template_id(template)} rule is missing required 'paths' specification: {rule}" + ) for path in rule["http"]["paths"]: - assert "pathType" in path + assert "pathType" in path, f"{template_id(template)} path is missing required 'pathType': {path}" # Exact would be ok, but ImplementationSpecifc is unacceptable as we don't know the implementation - assert path["pathType"] == "Prefix" + assert path["pathType"] == "Prefix", ( + f"{template_id(template)} path uses pathType '{path['pathType']}' instead of 'Prefix': {path}" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_no_ingress_annotations_by_default(templates): + """ + Ingress resources must not have annotations by default. + + When no specific annotations are configured in Helm values, Ingress resources + should not include an annotations field. This ensures clean default configurations + and prevents unexpected behavior from unspecified annotations. + """ for template in templates: if template["kind"] == "Ingress": - assert "annotations" not in template["metadata"] + assert "annotations" not in template["metadata"], ( + f"{template_id(template)} has annotations but none were configured" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_renders_component_ingress_annotations(values, make_templates): + """ + Ingress resources must render component-specific annotations. + + When Helm values configure annotations at the component level (per-deployable), + the generated Ingress resources must include those annotations in their metadata. + This allows fine-grained control over ingress behavior per component. + """ + def set_annotations(deployable_details: DeployableDetails): deployable_details.set_helm_values( values, @@ -122,28 +179,61 @@ def set_annotations(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "annotations" in template["metadata"] - assert "component" in template["metadata"]["annotations"] - assert template["metadata"]["annotations"]["component"] == "set" + assert "annotations" in template["metadata"], ( + f"{template_id(template)} is missing annotations despite component annotations being configured" + ) + assert "component" in template["metadata"]["annotations"], ( + f"{template_id(template)} is missing 'component' annotation: {template['metadata']['annotations']}" + ) + assert template["metadata"]["annotations"]["component"] == "set", ( + f"{template_id(template)} has incorrect component annotation value: " + f"{template['metadata']['annotations']['component']}" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_renders_global_ingress_annotations(values, make_templates): + """ + Ingress resources must render global annotations. + + When Helm values configure annotations at the global level ($.ingress.annotations), + all generated Ingress resources must include those annotations in their metadata. + This allows chart-wide control over ingress behavior. + """ values.setdefault("ingress", {})["annotations"] = { "global": "set", } for template in await make_templates(values): if template["kind"] == "Ingress": - assert "annotations" in template["metadata"] - assert "global" in template["metadata"]["annotations"] - assert template["metadata"]["annotations"]["global"] == "set" + assert "annotations" in template["metadata"], ( + f"{template_id(template)} is missing annotations despite global annotations being configured" + ) + assert "global" in template["metadata"]["annotations"], ( + f"{template_id(template)} is missing 'global' annotation: {template['metadata']['annotations']}" + ) + assert template["metadata"]["annotations"]["global"] == "set", ( + f"{template_id(template)} has incorrect global annotation value: " + f"{template['metadata']['annotations']['global']}" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_merges_global_and_component_ingress_annotations(values, make_templates): + """ + Ingress resources must properly merge global and component annotations. + + When both global ($.ingress.annotations) and component-level annotations are configured: + - Component annotations take precedence over global annotations + - Component annotations with null values should override global annotations + (expecting to delete the global annotation from the component annotation) + - All annotations should be properly merged in the final Ingress metadata + + This ensures proper annotation precedence and merging behavior. + """ + def set_annotations(deployable_details: DeployableDetails): deployable_details.set_helm_values( values, @@ -165,43 +255,86 @@ def set_annotations(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "annotations" in template["metadata"] - assert "component" in template["metadata"]["annotations"] - assert template["metadata"]["annotations"]["component"] == "set" + assert "annotations" in template["metadata"], ( + f"{template_id(template)} is missing annotations despite both global and" + "component annotations being configured" + ) + assert "component" in template["metadata"]["annotations"], ( + f"{template_id(template)} is missing 'component' annotation: {template['metadata']['annotations']}" + ) + assert template["metadata"]["annotations"]["component"] == "set", ( + f"{template_id(template)} has incorrect component annotation value: " + f"{template['metadata']['annotations']['component']}" + ) - assert "merged" in template["metadata"]["annotations"] - assert template["metadata"]["annotations"]["merged"] == "from_component" + assert "merged" in template["metadata"]["annotations"], ( + f"{template_id(template)} is missing 'merged' annotation: {template['metadata']['annotations']}" + ) + assert template["metadata"]["annotations"]["merged"] == "from_component", ( + f"{template_id(template)} has incorrect merged annotation value (should be from component): " + f"{template['metadata']['annotations']['merged']}" + ) # The key is still in the template but it renders as null (Python None) # And the k8s API will then filter it out - assert "global" in template["metadata"]["annotations"] - assert template["metadata"]["annotations"]["global"] is None + assert "global" in template["metadata"]["annotations"], ( + f"{template_id(template)} is missing 'global' annotation: {template['metadata']['annotations']}" + ) + assert template["metadata"]["annotations"]["global"] is None, ( + f"{template_id(template)} has incorrect global annotation value (should be None/null): " + f"{template['metadata']['annotations']['global']}" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_no_ingress_tlsSecret_global(make_templates, values): + """ + Ingress resources must not include TLS configuration when TLS is disabled globally. + + When $.ingress.tlsEnabled is set to false, no Ingress resources should include + TLS configuration in their spec. + """ values.setdefault("ingress", {})["tlsEnabled"] = False for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" not in template["spec"] + assert "tls" not in template["spec"], ( + f"{template_id(template)} has TLS configuration despite global tlsEnabled being false" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_no_ingress_tlsSecret_beats_global(make_templates, values): + """ + Component-level TLS disabled setting must override global TLS enabled setting. + + When a component specifically sets tlsEnabled: false, it should override any + global $.ingress.tlsEnabled: true setting. This allows per-component control over TLS. + """ + def set_tls_disabled(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"tlsEnabled": False}) iterate_deployables_ingress_parts(set_tls_disabled) for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" not in template["spec"] + assert "tls" not in template["spec"], ( + f"{template_id(template)} has TLS configuration despite component tlsEnabled being false" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_uses_component_ingress_tlsSecret(values, make_templates): + """ + Ingress resources must use component-specific TLS secrets when configured. + + When a component sets a specific tlsSecret in its ingress configuration, + the generated Ingress must use that secret name in its TLS configuration. + This allows per-component TLS certificate management. + """ + def set_tls_secret(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"tlsSecret": "component"}) @@ -209,30 +342,69 @@ def set_tls_secret(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" in template["spec"] - assert len(template["spec"]["tls"]) == 1 - assert len(template["spec"]["tls"][0]["hosts"]) == 1 - assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"] - assert template["spec"]["tls"][0]["secretName"] == "component" + assert "tls" in template["spec"], ( + f"{template_id(template)} is missing TLS configuration despite component tlsSecret being set" + ) + assert len(template["spec"]["tls"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'])} TLS configurations, expected only 1" + ) + assert len(template["spec"]["tls"][0]["hosts"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'][0]['hosts'])} TLS hosts, expected only 1" + ) + assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"], ( + f"{template_id(template)} TLS host {template['spec']['tls'][0]['hosts'][0]} " + f"does not match ingress host {template['spec']['rules'][0]['host']}" + ) + assert template["spec"]["tls"][0]["secretName"] == "component", ( + f"{template_id(template)} TLS secretName is {template['spec']['tls'][0]['secretName']}, " + "expected 'component'" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_uses_global_ingress_tlsSecret(values, make_templates): + """ + Ingress resources must use global TLS secrets when no component-specific secret is configured. + + When $.ingress.tlsSecret is set globally and no component-specific tlsSecret is configured, + all Ingress resources must use the global TLS secret name in their TLS configuration. + This allows chart-wide TLS certificate management. + """ values.setdefault("ingress", {})["tlsSecret"] = "global" for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" in template["spec"] - assert len(template["spec"]["tls"]) == 1 - assert len(template["spec"]["tls"][0]["hosts"]) == 1 - assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"] - assert template["spec"]["tls"][0]["secretName"] == "global" + assert "tls" in template["spec"], ( + f"{template_id(template)} is missing TLS configuration despite global tlsSecret being set" + ) + assert len(template["spec"]["tls"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'])} TLS configurations, expected only 1" + ) + assert len(template["spec"]["tls"][0]["hosts"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'][0]['hosts'])} TLS hosts, expected only 1" + ) + assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"], ( + f"{template_id(template)} TLS host {template['spec']['tls'][0]['hosts'][0]} " + f"does not match ingress host {template['spec']['rules'][0]['host']}" + ) + assert template["spec"]["tls"][0]["secretName"] == "global", ( + f"{template_id(template)} TLS secretName is {template['spec']['tls'][0]['secretName']}, " + "expected 'global'" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_component_ingress_tlsSecret_beats_global(values, make_templates): + """ + Component-specific TLS secrets must override global TLS secrets. + + When both global ($.ingress.tlsSecret) and component-specific tlsSecret are configured, + the component-specific secret must take precedence. This allows per-component override + of chart-wide TLS certificate settings. + """ + def set_tls_secret(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"tlsSecret": "component"}) @@ -241,34 +413,75 @@ def set_tls_secret(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" in template["spec"] - assert len(template["spec"]["tls"]) == 1 - assert len(template["spec"]["tls"][0]["hosts"]) == 1 - assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"] - assert template["spec"]["tls"][0]["secretName"] == "component" + assert "tls" in template["spec"], ( + f"{template_id(template)} is missing TLS configuration despite both " + "global and component tlsSecret being set" + ) + assert len(template["spec"]["tls"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'])} TLS configurations, expected only 1" + ) + assert len(template["spec"]["tls"][0]["hosts"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'][0]['hosts'])} TLS hosts, expected only 1" + ) + assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"], ( + f"{template_id(template)} TLS host {template['spec']['tls'][0]['hosts'][0]} " + f"does not match ingress host {template['spec']['rules'][0]['host']}" + ) + assert template["spec"]["tls"][0]["secretName"] == "component", ( + f"{template_id(template)} TLS secretName is {template['spec']['tls'][0]['secretName']}, " + f"expected 'component' (component should override global 'global')" + ) @pytest.mark.parametrize("values_file", values_files_to_test - services_values_files_to_test) @pytest.mark.asyncio_cooperative async def test_tls_no_secretName_by_default(templates): + """ + TLS configurations must not include secretName by default when using cert-manager. + + When cert-manager is configured and no explicit tlsSecret is set, TLS configurations + should not include a secretName field. cert-manager will automatically create and manage + the TLS secret based on the configured issuer. + """ for template in templates: if template["kind"] == "Ingress": - assert "tls" in template["spec"] + assert "tls" in template["spec"], f"{template_id(template)} is missing TLS configuration" for tls_spec in template["spec"]["tls"]: - assert "secretName" not in tls_spec + assert "secretName" not in tls_spec, ( + f"{template_id(template)} TLS configuration has secretName {tls_spec.get('secretName')}, " + f"but no explicit tlsSecret was configured (cert-manager should manage this)" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_no_ingressClassName_by_default(templates): + """ + Ingress resources must not have ingressClassName by default. + + When no specific ingress class is configured, Ingress resources should not include + an ingressClassName field. This allows the default ingress controller to handle + the ingress resources according to cluster configuration. + """ for template in templates: if template["kind"] == "Ingress": - assert "ingressClassName" not in template["spec"] + assert "ingressClassName" not in template["spec"], ( + f"{template_id(template)} has ingressClassName {template['spec'].get('ingressClassName')}, " + f"but no ingress class was configured" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_uses_component_ingressClassName(values, make_templates): + """ + Ingress resources must use component-specific ingress class names when configured. + + When a component sets a specific className in its ingress configuration, + the generated Ingress must use that class name in its spec.ingressClassName field. + This allows per-component selection of ingress controllers. + """ + def set_ingress_className(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"className": "component"}) @@ -276,24 +489,48 @@ def set_ingress_className(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "ingressClassName" in template["spec"] - assert template["spec"]["ingressClassName"] == "component" + assert "ingressClassName" in template["spec"], ( + f"{template_id(template)} is missing ingressClassName despite component className being configured" + ) + assert template["spec"]["ingressClassName"] == "component", ( + f"{template_id(template)} ingressClassName is {template['spec']['ingressClassName']}, " + "expected 'component'" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_uses_global_ingressClassName(values, make_templates): + """ + Ingress resources must use global ingress class names when no component-specific class is configured. + + When $.ingress.className is set globally and no component-specific className is configured, + all Ingress resources must use the global class name in their spec.ingressClassName field. + This allows chart-wide selection of ingress controllers. + """ values.setdefault("ingress", {})["className"] = "global" for template in await make_templates(values): if template["kind"] == "Ingress": - assert "ingressClassName" in template["spec"] - assert template["spec"]["ingressClassName"] == "global" + assert "ingressClassName" in template["spec"], ( + f"{template_id(template)} is missing ingressClassName despite global className being configured" + ) + assert template["spec"]["ingressClassName"] == "global", ( + f"{template_id(template)} ingressClassName is {template['spec']['ingressClassName']}, expected 'global'" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_component_ingressClassName_beats_global(values, make_templates): + """ + Component-specific ingress class names must override global ingress class names. + + When both global ($.ingress.className) and component-specific className are configured, + the component-specific class name must take precedence. This allows per-component override + of chart-wide ingress controller selection. + """ + def set_ingress_className(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"className": "component"}) @@ -302,13 +539,30 @@ def set_ingress_className(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "ingressClassName" in template["spec"] - assert template["spec"]["ingressClassName"] == "component" + assert "ingressClassName" in template["spec"], ( + f"{template_id(template)} is missing ingressClassName despite both " + "global and component className being configured" + ) + assert template["spec"]["ingressClassName"] == "component", ( + f"{template_id(template)} ingressClassName is {template['spec']['ingressClassName']}, " + f"expected 'component' (component should override global 'global')" + ) @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_ingress_services_global_service_properties(values, make_templates): + """ + Ingress backend services must use global service properties when configured. + + When global ingress service properties are configured ($.ingress.service.*): + - Backend services must use the specified service type (LoadBalancer) + - Services must apply the configured traffic policies (internalTrafficPolicy, externalTrafficPolicy) + - Services must include global annotations + - Services must target named ports + + This ensures consistent service configuration across all ingress-backed services. + """ values.setdefault("ingress", {}).setdefault("service", {})["type"] = "LoadBalancer" values.setdefault("ingress", {}).setdefault("service", {})["internalTrafficPolicy"] = "Local" values.setdefault("ingress", {}).setdefault("service", {})["externalTrafficPolicy"] = "Local" @@ -362,6 +616,17 @@ async def test_ingress_services_global_service_properties(values, make_templates @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_merges_global_and_component_ingress_services_annotations(values, make_templates): + """ + Ingress backend services must properly merge global and component service annotations. + + When both global ($.ingress.service.annotations) and component-level service annotations are configured: + - Component annotations take precedence over global annotations + - Component annotations with null values should override global annotations + - All annotations should be properly merged in the final Service metadata + + This ensures proper annotation precedence and merging behavior for ingress backend services. + """ + def set_annotations(deployable_details: DeployableDetails): deployable_details.set_helm_values( values, @@ -416,6 +681,18 @@ def set_annotations(deployable_details: DeployableDetails): @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_ingress_services_local_service_properties(values, make_templates): + """ + Ingress backend services must use component-specific service properties when configured. + + When component-specific ingress service properties are configured: + - Component properties must override global properties + - Services must use the specified service type (LoadBalancer) + - Services must apply the configured traffic policies + - Services must include component-specific externalIPs + - Services must include component-specific annotations + + This ensures per-component control over ingress backend service configuration. + """ values.setdefault("ingress", {}).setdefault("service", {})["type"] = "ClusterIP" values.setdefault("ingress", {}).setdefault("service", {})["internalTrafficPolicy"] = "Cluster" values.setdefault("ingress", {}).setdefault("service", {})["externalTrafficPolicy"] = "Cluster" @@ -504,6 +781,16 @@ def set_ingress_service_properties(deployable_details: DeployableDetails, extern @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_ingress_certManager_clusterissuer(make_templates, values): + """ + Ingress resources must include cert-manager cluster issuer annotations when configured. + + When certManager.clusterIssuer is configured: + - Ingress resources must include cert-manager.io/cluster-issuer annotation + - The annotation must reference the configured cluster issuer name + - TLS configuration must use the expected secret name format for cert-manager + + This enables automatic TLS certificate provisioning via cert-manager using cluster issuers. + """ values["certManager"] = {"clusterIssuer": "cluster-issuer-name"} for template in await make_templates(values): if template["kind"] == "Ingress": @@ -519,6 +806,16 @@ async def test_ingress_certManager_clusterissuer(make_templates, values): @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_ingress_certManager_issuer(make_templates, values): + """ + Ingress resources must include cert-manager issuer annotations when configured. + + When certManager.issuer is configured: + - Ingress resources must include cert-manager.io/issuer annotation + - The annotation must reference the configured issuer name + - TLS configuration must use the expected secret name format for cert-manager + + This enables automatic TLS certificate provisioning via cert-manager using namespace issuers. + """ values["certManager"] = {"issuer": "issuer-name"} for template in await make_templates(values): if template["kind"] == "Ingress": @@ -534,6 +831,17 @@ async def test_ingress_certManager_issuer(make_templates, values): @pytest.mark.parametrize("values_file", values_files_to_test) @pytest.mark.asyncio_cooperative async def test_component_ingress_tlsSecret_beats_certManager(values, make_templates): + """ + Component-specific TLS secrets must override cert-manager configuration. + + When both certManager is configured and a component sets a specific tlsSecret: + - The component-specific tlsSecret must be used + - No cert-manager annotations should be present + - The TLS configuration must use the component-specific secret name + + This allows manual TLS certificate management to override automatic cert-manager provisioning. + """ + def set_tls_secret(deployable_details: DeployableDetails): deployable_details.set_helm_values(values, PropertyType.Ingress, {"tlsSecret": "component"}) @@ -542,9 +850,24 @@ def set_tls_secret(deployable_details: DeployableDetails): for template in await make_templates(values): if template["kind"] == "Ingress": - assert "tls" in template["spec"] - assert len(template["spec"]["tls"]) == 1 - assert len(template["spec"]["tls"][0]["hosts"]) == 1 - assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"] - assert template["spec"]["tls"][0]["secretName"] == "component" - assert not template["metadata"].get("annotations") + assert "tls" in template["spec"], ( + f"{template_id(template)} is missing TLS configuration despite component tlsSecret being set" + ) + assert len(template["spec"]["tls"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'])} TLS configurations, expected only 1" + ) + assert len(template["spec"]["tls"][0]["hosts"]) == 1, ( + f"{template_id(template)} has {len(template['spec']['tls'][0]['hosts'])} TLS hosts, expected only 1" + ) + assert template["spec"]["tls"][0]["hosts"][0] == template["spec"]["rules"][0]["host"], ( + f"{template_id(template)} TLS host {template['spec']['tls'][0]['hosts'][0]} " + f"does not match ingress host {template['spec']['rules'][0]['host']}" + ) + assert template["spec"]["tls"][0]["secretName"] == "component", ( + f"{template_id(template)} TLS secretName is {template['spec']['tls'][0]['secretName']}, " + "expected 'component'" + ) + assert not template["metadata"].get("annotations"), ( + f"{template_id(template)} has cert-manager annotations despite component tlsSecret being set: " + f"{template['metadata'].get('annotations')}" + )