Hedronite · Dev Lesson · Polyglot-Dev / Python · Thu 2026-09-03

Python and the Kubernetes Ingress Census — the path that is not a Service

The client lists Ingress objects. host, path, backend are attributes. Your loop is not a patch.

Lesson Class: Dev (Python + Kubernetes client NetworkingV1Api)
Cloud Referent: AKS leftover is a path without an Ingress; this client cannot see Application Gateway
Paired Ops: AKS plus Application Gateway; refuse a second LoadBalancer Service
Paired Cert: CKA Q12 jsonpaths the checker must not write
Grounding: K8sUR3 Ch.13 p.216 · Ch.7 Ingress · Q12 validate jsonpaths
List
Ingress rules, host, path, pathType, backend, class.
Tags
class_empty / host_empty / path_root / backend_missing.
No patch
Q12 writes one object. A loop routes the fleet.
The client lists Ingress objects. host, path, backend are attributes. Your loop is not a patch.

<!-- hal:authoritative:yaml -->

The client lists Ingress objects. host, path, backend are attributes. Your loop is not a patch.

§I — Frame

The day's Ops lesson names the leftover an AKS cluster that already had a Load Balancer Service while Application Gateway sat unused, and an Ingress object nobody applied. One listener. Many paths. A second --type=LoadBalancer is 07-29. A second cluster is 08-28. Neither is today's read.

This slot is Python touching K8s. Cert is CKA Ingress Q12. Dev stays on the client. 08-31 already listed Pods and printed privileged, ape, seccomp, apparmor, and refused to patch. 08-28 already listed Deployments and owned ReplicaSets and refused to patch the image. 08-25 already listed ClusterRoleBindings and refused to delete. 08-22 already listed PersistentVolumes and refused to patch claimRef. 07-29 already watched EndpointSlices as a client-side registry. None of those tools answer the question Ops just asked: which Ingress objects exist, which host and path they claim, which Service they point at, and is anyone about to patch a rule from a loop?

Call the failure the census that must not route. A first draft will read host is None and call patch_namespaced_ingress so example.org lands. That is Cert's apply on one object. It is not a census. Q12 LabSetUp plants a Deployment so a candidate can expose a Service and create an Ingress. A cluster-wide loop that does the same thing writes a path a canary is mid-flight on, writes a path a debug Ingress needed as a catch-all, and writes a path you needed as evidence. Image patch was 08-28. SecurityContext patch was 08-31. Today the wrong write is a rule you did not mean to mint.

Today's tool does one read. NetworkingV1Api.list_ingress_for_all_namespaces for spec. It prints rows whose host is empty, whose path is missing, whose backend service is missing, whose class is empty on a cluster that has two controllers. It does not patch. It does not create. It does not call kubectl apply. It does not call az network application-gateway. The listener is a different client.

§II — Language idiom: the generated networking object

K8sUR3 Ch. 13 is blunt about how you talk to this API. The OpenAPI spec is the source. The generated client libraries are how a language consumes it (p. 216). Python's kubernetes package is that generated client. 08-28 used AppsV1Api because Deployment lives on apps/v1. 08-31 used CoreV1Api because Pod lives on core v1. 08-25 used RbacAuthorizationV1Api. Ingress lives on networking.k8s.io/v1. The client is NetworkingV1Api. There is no CoreV1Api.list_ingress_for_all_namespaces. Do not mix the two clients and then wonder why the method is missing.

K8sUR3 Ch. 7 names the split (PDF pp. 110-125). Specification versus controller. Host. Path. Longest prefix. Multiple controllers. The generated class is V1Ingress. spec.rules is a list of V1IngressRule. Each rule has host and http.paths. Each path is V1HTTPIngressPath with path, path_type, and backend. Backend on networking.k8s.io/v1 is V1IngressServiceBackend: service.name and service.port.number (or service.port.name). The old extensions/v1beta1 backend was a flat serviceName and servicePort. A census that reads path.backend.service_name is reading a field that does not exist on v1. AttributeError is the honest failure. A dict silently returns None for a typo.

Load config the way 08-04 already taught. In-cluster uses the projected token. Out-of-cluster uses kubeconfig. Today's tool does not care which, so long as the ServiceAccount can get and list ingresses. A patch verb is a bug. An update verb is a bug. A create on Gateway is a bug in a different API (Q11 leftover). The Role that owns this tool lists one resource and two verbs. Listing Services is a second Role, optional, only to report type. It is still not a write.

from kubernetes import client, config

def networking() -> client.NetworkingV1Api:
    try:
        config.load_incluster_config()
    except config.ConfigException:
        config.load_kube_config()
    return client.NetworkingV1Api()

The list call returns V1IngressList. Each item is a V1Ingress. Prefer the attributes. They fail loud when the generated class changes.

spec is a V1IngressSpec or None. spec.ingress_class_name is the field K8sUR3 p. 120 spends on multiple controllers. spec.rules may be None. A default backend may exist with zero rules. Print both. Do not treat missing rules as "no Ingress." The object still exists. ADDRESS empty is status, not spec. Status is how you see a missing controller. Spec is how Q12 grades you.

Q12 validate.bash reads four jsonpaths:

.spec.rules[0].host
.spec.rules[0].http.paths[0].path
.spec.rules[0].http.paths[0].backend.service.name
.spec.rules[0].http.paths[0].backend.service.port.number

A census that only prints .status.load_balancer.ingress[0].ip will call Q12 green when ADDRESS is populated and miss a host of example.com. The exam wanted example.org. Status is the listener. Spec is the path.

§III — Code worked example: print the rule, refuse the patch

Walk every Ingress. Walk every rule. Walk every path. Tag leftovers. Return rows. Do not call patch.

from dataclasses import dataclass
from typing import Iterator

from kubernetes import client

@dataclass(frozen=True)
class IngressRow:
    namespace: str
    name: str
    klass: str
    host: str
    path: str
    path_type: str
    backend: str
    port: str
    tags: tuple[str, ...]


def rows(api: client.NetworkingV1Api) -> Iterator[IngressRow]:
    listed = api.list_ingress_for_all_namespaces()
    for item in listed.items:
        spec = item.spec
        klass = (spec.ingress_class_name if spec else None) or ""
        rules = (spec.rules if spec else None) or [None]
        for rule in rules:
            host = (rule.host if rule else None) or ""
            paths = (rule.http.paths if rule and rule.http else None) or [None]
            for path in paths:
                yield _row(item, klass, host, path)


def _row(item, klass, host, path) -> IngressRow:
    pth = (path.path if path else None) or ""
    ptype = (path.path_type if path else None) or ""
    svc = path.backend.service if path and path.backend else None
    name = (svc.name if svc else None) or ""
    port_obj = svc.port if svc else None
    number = port_obj.number if port_obj else None
    port_name = port_obj.name if port_obj else None
    port = str(number) if number is not None else (port_name or "")
    tags = []
    if not klass:
        tags.append("class_empty")
    if not host:
        tags.append("host_empty")
    if not pth:
        tags.append("path_empty")
    if pth == "/":
        tags.append("path_root")
    if not name:
        tags.append("backend_missing")
    if not port:
        tags.append("port_missing")
    return IngressRow(
        namespace=item.metadata.namespace,
        name=item.metadata.name,
        klass=klass,
        host=host,
        path=pth,
        path_type=ptype,
        backend=name,
        port=port,
        tags=tuple(tags),
    )

Print CSV or JSON. The tags are the leftover. class_empty is Poulton's two-controller trap and K8sUR3 p. 120. On a kubeadm exam with one nginx, it is noise. On AKS with Application Gateway and a leftover nginx, it is the row you wanted. Do not auto-fill azure-application-gateway because Ops named Application Gateway. That write is a class the stem did not give you.

host_empty matches every host. Sometimes you wanted that. Q12 did not. path_root is the candidate who typed / and hoped Prefix would cover /echo. validate.bash fails them. The census reports them. backend_missing is an Ingress that is not a path to a Service. That is the coin as a bug: a path that is not a Service, accidentally.

Join the Service only after the Ingress row exists. Optional. CoreV1Api.read_namespaced_service(backend, namespace). Print spec.type. Q12 wanted NodePort. Production behind Application Gateway often wants ClusterIP. The census does not flip the type. 07-29 already taught expose. Today reports the join.

What the loop does not do:

def forbidden(api: client.NetworkingV1Api, row: IngressRow) -> None:
    raise RuntimeError("census does not patch %s/%s" % (row.namespace, row.name))

No patch_namespaced_ingress. No create_namespaced_ingress. No NetworkingV1Api Gateway objects; those are CustomObjectsApi on gateway.networking.k8s.io and they are Q11. No subprocess kubectl. 08-28 already spent "the census that must not roll." Same spine. Different group.

A tiny main so you can run it against the exam cluster without inventing a framework.

def main() -> None:
    api = networking()
    print("ns,name,class,host,path,pathType,backend,port,tags")
    for row in rows(api):
        print(
            ",".join(
                [
                    row.namespace,
                    row.name,
                    row.klass,
                    row.host,
                    row.path,
                    row.path_type,
                    row.backend,
                    row.port,
                    "|".join(row.tags),
                ]
            )
        )

If echo,echo-sound is missing entirely, Q12 is not started. If it is present with host example.org, path /echo, backend echo-service, port 8080, Q12 spec is green. If ADDRESS is empty, the controller is missing. That is Ops fact two. The Python client cannot install nginx and cannot create Application Gateway. Print status as a second table if you must. Do not treat empty ADDRESS as a reason to patch spec.

A second table for status is legal if it stays a table. item.status.load_balancer.ingress is a list of V1IngressLoadBalancerIngress with ip or hostname. Print it next to the spec row. Empty list means no controller has claimed the object, or the controller has not programmed a listener yet. That is Ops fact two as a column. It is not a reason to copy host from DNS into spec. It is not a reason to create a Service of type LoadBalancer so that some address appears. 07-29 already made that EXTERNAL-IP. Q12 validate.bash never greps ADDRESS. A census that fails rows with empty ADDRESS will fail the exam cluster before the candidate has applied nginx, and will fail a correctly spec'd Ingress on kubeadm for ten seconds after apply. Spec first. Status second. Patch never.

The v1beta1 hangover is the other silent None. Old snippets still show:

backend:
  serviceName: echo-service
  servicePort: 8080

The generated v1 class stores that under path.backend.service.name and path.backend.service.port.number. path.backend.service_name does not exist. getattr(path.backend, "service_name", None) hides the miss and prints backend_missing on a valid Q12 object. Drop getattr for this field. Let AttributeError stop the run when you point at the old name on purpose in a unit test. Production census uses the v1 attributes only.

Port can be a name. Q12 uses a number. A Service that exposes name: http with port: 8080 can be referenced as port.name: http. Print whichever is set. Tag port_missing only when both are empty. Do not coerce http to 80. Do not coerce 8080 to http. validate.bash compares the number 8080. A census that prints http on that object is still honest if the Ingress used the name. Q12 did not.

Default backend is a fifth jsonpath nobody asked for today and you should still print when present: spec.default_backend.service.name. Traffic that matches no rule lands there. Q12 has rules and no default. A fleet Ingress that has only a default backend and no host is a catch-all. Tag it default_only. That is a path without a host. It is still a path. It is still not a Service. The Service is the backend. The Ingress is the rule that selected it.

TLS hosts are adjacent and 08-01 already spent them. spec.tls[i].hosts and spec.tls[i].secret_name can print as a third table. Do not open the Secret. Do not call CoreV1Api.read_namespaced_secret. 08-19 already taught a checker that cannot see etcd. Today's checker does not need the PEM. If tls is empty and the stem did not ask, the row is not a leftover. If tls is empty and the stem named HTTPS, that is Q11's migrate-the-existing-TLS job, not Q12, and not this loop's write.

§IV — Connection to today's Ops lesson

Ops coined the path that is not a Service. Application Gateway is the listener az-900 named. AKS is the cluster the cheatsheet named. The Ingress object is the rule. This file is the reader of that rule.

Ops refused a second LoadBalancer Service named as if it were Ingress. The census will see that Service if you list Services. It will not see it if you only list Ingress. That is the point of the client choice. NetworkingV1Api does not return Service objects. A leftover checker that lists Services and greps LoadBalancer is 07-29's tool. Use it as a join, not as the scan.

Ops refused az network application-gateway as today's apply. This client refuses it too. There is no Azure SDK in this file. 08-23 already used Key Vault as a Python properties census and refused get_secret. Same posture. Different object.

If the census prints class_empty on every row, read Ops fact two before you "fix" it. One controller watching all Ingress objects is legal. Two controllers without a class is a coin flip. The Python loop does not know how many controllers exist. kubectl get ingressclass is a different list. You may add it as a second call. You may not pick a winner.

§V — Prior-lesson reach

08-31 listed Pods. Container securityContext wins over Pod. AttributeError on pod.spec.security_context.privileged was the honest failure. Today the honest failure is path.backend.service_name on a v1 object. Container-versus-Pod is not this API. Rule-versus-path is. Walk both.

08-28 listed Deployments and owned ReplicaSets. CURRENT above DESIRED was the surge. Your loop was not a patch. Today's CURRENT is not a replica count. A second path on the same host is not a surge. It is longest prefix (K8sUR3 PDF p. 120). Do not import rollout math.

08-25 listed ClusterRoleBindings and refused to delete. Today's Role is get/list on ingresses. A bind that includes patch is a bug in the YAML that ships with the tool, not a bug in the loop. Check the Role.

07-29 watched EndpointSlices. That is how you see whether echo-service has addresses. Q12 validate check 8 does that with kubectl get endpoints. You may call CoreV1Api.read_namespaced_endpoints as a join. You may not recreate the 07-29 watch loop and call it Ingress.

08-19 read encryptionConfig and could not see etcd. Today's analogue: the Python client can see the Ingress object and cannot see Application Gateway's HTTP settings. Empty ADDRESS is the closest status. Do not pretend status.load_balancer is the Azure resource.

§VI — Close

The client is NetworkingV1Api. The object is V1Ingress. The fields are host, path, pathType, backend name, backend port, class. Q12's jsonpaths are the grade on the exam. The tags are the leftover on the fleet. The loop does not patch. The loop does not create Gateway. The loop does not mint a Service.

Read the Ingress first. Then join the Service if you need type. Then look at ADDRESS if you need the controller. Decide whether you are on the exam or on the fleet. The exam wants four jsonpaths. The fleet wants one class on a cluster that has two. Neither wants a patch from a census.

Examine well. The path is not a Service.

Related