"""CKAD tasks 65-76: Services and Networking - the pool tasks that take the domain from 5 to 17. 5.1 NetworkPolicies 65 default deny, then one allow 66 namespace AND pod selector 67 a policy that lets nothing through 68 same Namespace only 69 get through policies you may not change 5.2 Services 70 target port by name, sticky sessions 71 ExternalName 72 endpoints that never become ready 73 a Service without a selector 74 narrow a selector to one version 5.3 Ingress 75 two hosts and a default backend 76 Exact and Prefix paths, rewrite Every NetworkPolicy and Service task is tested with real traffic (kindnet enforces NetworkPolicy). A check that something is blocked only counts while the task's own work is in place and the allowed path demonstrably works - a connection that fails proves nothing by itself, and a task must score nothing before anyone has worked on it. No Ingress controller runs here: 75 and 76 are graded on the object. Each task has a scripted solution in solve.py, which `./ksim audit` runs on the host. """ from sim.bank import C, NS, ns_pick, name_pick # Traffic probes for the checks. ok NS POD URL one try; okr NS POD URL a few, for a path # that should work (a policy or an endpoint takes a moment to reach the node). NET = r""" ok() { $K -n "$1" exec "$2" -- wget -T 3 -qO- "$3" >/dev/null 2>&1; } okr() { for _ in 1 2 3 4; do ok "$@" && return 0; sleep 2; done; return 1; } # blocked NS POD URL: the request fails - at once, or once a policy applied a moment ago has landed blocked() { for _ in 1 2 3; do ok "$@" || return 0; sleep 2; done; return 1; } """ # Scenario building blocks for the setups. srv NS NAME [LABELS] an nginx Pod with a Service of # its name on port 80; cli NS NAME [LABELS] a busybox Pod to send requests from. PODS = NS + r""" srv() { $K -n "$1" run "$2" --image=nginx:1.27 --port=80 --labels="${3:-app=$2}" >/dev/null $K -n "$1" expose pod "$2" --port=80 >/dev/null } cli() { $K -n "$1" run "$2" --image=busybox:1.36 --labels="${3:-app=$2}" --command -- sleep 86400 >/dev/null; } ready() { $K -n "$1" wait --for=condition=Ready pod --all --timeout=240s >/dev/null; } """ # An Ingress as JSON on stdin of a python one-liner, for the object checks. ING = r""" ing() { $K -n "$1" get ingress "$2" -o json 2>/dev/null | python3 -c "$3"; } """ QUESTIONS = [ # ============================================================ 5.1 NetworkPolicy ==== { "id": 65, "domain": "NET", "title": "Default deny, then one way in", "params": lambda r: {"ns": ns_pick(r, "shop")}, "task": """ Namespace `%{ns}` runs Pods `web`, `batch`, `api` and `db` (labels `app=`), and today every one of them accepts connections from anywhere. 1. Create NetworkPolicy `default-deny-ingress` that blocks **all incoming** traffic to **every** Pod of the Namespace. 2. Create NetworkPolicy `api-from-web` that lets Pods labelled `app=api` accept TCP port 80 from Pods labelled `app=web`, and from nothing else. Do not restrict outgoing traffic. kindnet enforces NetworkPolicy here, so both are tested with real requests. """, "setup": PODS + r""" mkns %{ns} srv %{ns} api; srv %{ns} db; cli %{ns} web; cli %{ns} batch ready %{ns} """, "checks": [ C("default-deny-ingress selects every Pod, for Ingress only, and allows nothing", 1, r""" eq "$(j -n %{ns} netpol default-deny-ingress '{.spec.podSelector}')" '{}' \ && eq "$(j -n %{ns} netpol default-deny-ingress '{.spec.policyTypes}')" '["Ingress"]' \ && [ -z "$(j -n %{ns} netpol default-deny-ingress '{.spec.ingress[*]}')" ]"""), C("web reaches api on port 80", 2, NET + r""" [ -n "$(j -n %{ns} netpol api-from-web '{.metadata.name}')" ] && okr %{ns} web http://api"""), C("batch can no longer reach api", 2, NET + r""" [ -n "$(j -n %{ns} netpol api-from-web '{.metadata.name}')" ] && okr %{ns} web http://api && blocked %{ns} batch http://api"""), C("nothing reaches db any more", 1, NET + r""" [ -n "$(j -n %{ns} netpol default-deny-ingress '{.metadata.name}')" ] && okr %{ns} web http://api && blocked %{ns} web http://db"""), C("no policy of the Namespace restricts egress", 1, r""" [ -n "$(j -n %{ns} netpol '{.items[*].metadata.name}')" ] && ! j -n %{ns} netpol '{.items[*].spec.policyTypes}' | grep -q Egress"""), ], "solution": """ cat </dev/null srv %{ns} metrics cli %{ns}-mon scraper role=scraper; cli %{ns}-mon shell role=debug; cli %{ns}-dev scraper role=scraper ready %{ns}; ready %{ns}-mon; ready %{ns}-dev """, "checks": [ C("metrics-scrape selects app=metrics, for Ingress only", 1, r""" eq "$(j -n %{ns} netpol metrics-scrape '{.spec.podSelector.matchLabels.app}')" metrics && eq "$(j -n %{ns} netpol metrics-scrape '{.spec.policyTypes}')" '["Ingress"]'"""), C("scraper in %{ns}-mon reaches metrics", 3, NET + r""" [ -n "$(j -n %{ns} netpol metrics-scrape '{.metadata.name}')" ] && okr %{ns}-mon scraper http://metrics.%{ns}"""), C("shell in %{ns}-mon (right Namespace, wrong label) is blocked", 2, NET + r""" [ -n "$(j -n %{ns} netpol metrics-scrape '{.metadata.name}')" ] && okr %{ns}-mon scraper http://metrics.%{ns} && blocked %{ns}-mon shell http://metrics.%{ns}"""), C("scraper in %{ns}-dev (right label, wrong Namespace) is blocked", 2, NET + r""" [ -n "$(j -n %{ns} netpol metrics-scrape '{.metadata.name}')" ] && okr %{ns}-mon scraper http://metrics.%{ns} && blocked %{ns}-dev scraper http://metrics.%{ns}"""), ], "solution": """ cat </dev/null <<'EOF' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: {name: cache-allow, namespace: %{ns}} spec: podSelector: {matchLabels: {app: cache}} policyTypes: [Ingress] ingress: - from: [{podSelector: {matchLabels: {role: worker}}}] ports: [{protocol: TCP, port: 8080}] EOF j -n %{ns} netpol cache-allow '{.metadata.uid}' > "$PRIV/q67.uid" ready %{ns} """, "checks": [ C("worker reaches cache", 3, NET + r"""okr %{ns} worker http://cache"""), C("cron is still kept out", 2, NET + r"""okr %{ns} worker http://cache && blocked %{ns} cron http://cache"""), C("cache-allow is the same object, and the only policy", 2, NET + r""" okr %{ns} worker http://cache || exit 1 eq "$(j -n %{ns} netpol cache-allow '{.metadata.uid}')" "$(cat "$PRIV/q67.uid")" && eq "$(j -n %{ns} netpol '{.items[*].metadata.name}')" cache-allow"""), ], "solution": """ k -n %{ns} describe netpol cache-allow # From: PodSelector role=worker To Port: 8080/TCP k -n %{ns} get pod --show-labels # the Pod says app=worker, not role=worker k -n %{ns} get pod cache -o jsonpath='{.spec.containers[0].ports}' # and cache listens on 80 k -n %{ns} edit netpol cache-allow # from: podSelector: matchLabels: role: worker -> app: worker # ports: port: 8080 -> 80 k -n %{ns} exec worker -- wget -T 3 -qO- http://cache | head -3 k -n %{ns} exec cron -- wget -T 3 -qO- http://cache # still times out # Two faults, one symptom. A policy port is the port of the Pod (the container port), never the # Service port, and a peer selector that matches no Pod is not an error - it just allows nobody. """, }, { "id": 68, "domain": "NET", "title": "Same Namespace only", "params": lambda r: {"ns": ns_pick(r, "tenant")}, "task": """ Namespace `%{ns}` belongs to one team. Its Pods must accept connections from **any Pod of the same Namespace**, and from **no other Namespace**. Create one NetworkPolicy `same-namespace` in `%{ns}` for that. It must cover every Pod the Namespace has now or gets later, and leave outgoing traffic alone. For testing: `front` and `back` run in `%{ns}` (`back` serves on port 80), and `visitor` runs in Namespace `%{ns}-guest`. """, "setup": PODS + r""" mkns %{ns}; mkns %{ns}-guest srv %{ns} back; cli %{ns} front; cli %{ns}-guest visitor ready %{ns}; ready %{ns}-guest """, "checks": [ C("same-namespace selects every Pod, for Ingress only", 2, r""" eq "$(j -n %{ns} netpol same-namespace '{.spec.podSelector}')" '{}' && eq "$(j -n %{ns} netpol same-namespace '{.spec.policyTypes}')" '["Ingress"]'"""), C("front reaches back", 2, NET + r""" [ -n "$(j -n %{ns} netpol same-namespace '{.metadata.name}')" ] && okr %{ns} front http://back"""), C("visitor from %{ns}-guest is blocked", 3, NET + r""" [ -n "$(j -n %{ns} netpol same-namespace '{.metadata.name}')" ] && okr %{ns} front http://back && blocked %{ns}-guest visitor http://back.%{ns}"""), ], "solution": """ cat </dev/null <<'EOF' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: {name: default-deny, namespace: %{ns}} spec: {podSelector: {}, policyTypes: [Ingress]} --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: {name: db-access, namespace: %{ns}} spec: podSelector: {matchLabels: {app: db}} policyTypes: [Ingress] ingress: - from: [{podSelector: {matchLabels: {access: db}}}] ports: [{protocol: TCP, port: 80}] EOF j -n %{ns} netpol '{range .items[*]}{.metadata.name}{" "}{.metadata.uid}{" "}{.metadata.generation}{"\n"}{end}' | sort > "$PRIV/q69.policies" ready %{ns} """, "checks": [ C("reporter reaches db", 3, NET + r"""okr %{ns} reporter http://db"""), C("the NetworkPolicies are exactly as they were", 2, NET + r""" okr %{ns} reporter http://db || exit 1 eq "$(j -n %{ns} netpol '{range .items[*]}{.metadata.name}{" "}{.metadata.uid}{" "}{.metadata.generation}{"\n"}{end}' | sort)" "$(cat "$PRIV/q69.policies")" """), C("stranger is still locked out", 2, NET + r"""okr %{ns} reporter http://db && blocked %{ns} stranger http://db"""), ], "solution": """ k -n %{ns} get netpol k -n %{ns} describe netpol db-access # From: PodSelector access=db k -n %{ns} get pod reporter --show-labels # app=reporter - no access label k -n %{ns} label pod reporter access=db k -n %{ns} exec reporter -- wget -T 3 -qO- http://db | head -3 # A policy names its peers by label, so the way in is to carry the label - no policy has to # change. In a Deployment the label goes into the Pod template (and rolls the Pods). """, }, # ================================================================== 5.2 Services ==== { "id": 70, "domain": "NET", "title": "A Service by port name, with sticky sessions", "params": lambda r: {"ns": ns_pick(r, "portal"), "app": name_pick(r, "portal"), "port": r.choice([8080, 9090, 8000])}, "task": """ Deployment `%{app}` in Namespace `%{ns}` runs two nginx Pods whose container port is declared with a **name**. Create ClusterIP Service `%{app}` for them: - Service port `%{port}`, named `web`, - forwarding to the container port **by its name**, not by its number, - with session affinity, so one client IP keeps reaching the same Pod. Pod `probe` in the Namespace is there to test with. """, "setup": PODS + r""" mkns %{ns} $K apply -f - >/dev/null <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: {name: %{app}, namespace: %{ns}} spec: replicas: 2 selector: {matchLabels: {app: %{app}}} template: metadata: {labels: {app: %{app}}} spec: containers: - name: web image: nginx:1.27 ports: [{name: http-main, containerPort: 80}] EOF cli %{ns} probe $K -n %{ns} rollout status deploy %{app} --timeout=240s >/dev/null ready %{ns} """, "checks": [ C("probe gets the nginx page from http://%{app}:%{port}", 2, NET + r""" okr %{ns} probe http://%{app}:%{port} && $K -n %{ns} exec probe -- wget -T 3 -qO- http://%{app}:%{port} 2>/dev/null | grep -qi nginx"""), C("port %{port} is named web and targets the container port by name", 2, r""" eq "$(j -n %{ns} svc %{app} '{.spec.ports[0].port} {.spec.ports[0].name} {.spec.ports[0].targetPort}')" "%{port} web http-main" """), C("session affinity by client IP", 1, r"""eq "$(j -n %{ns} svc %{app} '{.spec.sessionAffinity}')" ClientIP"""), C("ClusterIP Service with both Pods behind it", 1, r""" eq "$(j -n %{ns} svc %{app} '{.spec.type}')" ClusterIP || exit 1 [ "$(j -n %{ns} endpointslices -l kubernetes.io/service-name=%{app} '{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}' | grep -c .)" = 2 ]"""), ], "solution": """ k -n %{ns} get deploy %{app} -o jsonpath='{.spec.template.spec.containers[0].ports}' # name: http-main k -n %{ns} expose deploy %{app} --port=%{port} --target-port=http-main $do > svc.yaml vi svc.yaml # spec: # sessionAffinity: ClientIP # ports: # - name: web # port: %{port} # targetPort: http-main k apply -f svc.yaml k -n %{ns} exec probe -- wget -T 3 -qO- http://%{app}:%{port} | head -3 # A named targetPort follows the Pod: change the container port in the Deployment and the Service # keeps working. `expose` copies the Deployment's selector, which `create svc` would not. """, }, { "id": 71, "domain": "NET", "title": "A local name for a Service in another Namespace", "params": lambda r: {"ns": ns_pick(r, "orders")}, "task": """ Applications in Namespace `%{ns}` are configured to call `http://inventory`. The service they need is `warehouse` in Namespace `%{ns}-data`, and nothing may be deployed in `%{ns}` for it. 1. Create Service `inventory` in `%{ns}` of type **ExternalName** that points at the cluster DNS name of `warehouse` - the fully qualified one. 2. Write that fully qualified name to `{WS}/q71/target.txt`. Pod `probe` in `%{ns}` is there to test with. """, "setup": PODS + r""" mkns %{ns}; mkns %{ns}-data rm -rf "$WS/q71"; mkdir -p "$WS/q71" srv %{ns}-data warehouse; cli %{ns} probe ready %{ns}; ready %{ns}-data """, "checks": [ C("inventory is an ExternalName Service for warehouse.%{ns}-data.svc.cluster.local", 2, r""" eq "$(j -n %{ns} svc inventory '{.spec.type} {.spec.externalName}')" "ExternalName warehouse.%{ns}-data.svc.cluster.local" """), C("probe gets the page from http://inventory", 3, NET + r""" okr %{ns} probe http://inventory && $K -n %{ns} exec probe -- wget -T 3 -qO- http://inventory 2>/dev/null | grep -qi nginx"""), C("target.txt holds the fully qualified name", 1, r""" eq "$(tr -d ' \r\n' < "$WS/q71/target.txt" 2>/dev/null | sed 's/\.$//')" warehouse.%{ns}-data.svc.cluster.local"""), ], "solution": """ k -n %{ns} create service externalname inventory --external-name warehouse.%{ns}-data.svc.cluster.local echo warehouse.%{ns}-data.svc.cluster.local > {WS}/q71/target.txt k -n %{ns} exec probe -- nslookup inventory.%{ns}.svc.cluster.local # a CNAME to the warehouse name k -n %{ns} exec probe -- wget -T 3 -qO- http://inventory | head -3 # An ExternalName Service is only a DNS alias (a CNAME): no cluster IP, no selector, no proxying, # and no port mapping - the client still has to use the port the target serves on. The name # pattern is SERVICE.NAMESPACE.svc.cluster.local. """, }, { "id": 72, "domain": "NET", "title": "Endpoints that never become ready", "params": lambda r: {"ns": ns_pick(r, "stock"), "app": name_pick(r, "catalog")}, "task": """ In Namespace `%{ns}`, `wget http://%{app}` from Pod `probe` fails. Service `%{app}` is correct - do not change it - and the two Pods of Deployment `%{app}` are Running. 1. Find out why the Service has no ready endpoints, and fix the **Deployment** so both replicas serve. The Pods must keep a readiness probe that does an HTTP GET on port 80. 2. Write the HTTP status code the failing probe was getting to `{WS}/q72/code.txt`. """, "setup": PODS + r""" mkns %{ns}; rm -rf "$WS/q72"; mkdir -p "$WS/q72" $K apply -f - >/dev/null <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: {name: %{app}, namespace: %{ns}} spec: replicas: 2 selector: {matchLabels: {app: %{app}}} template: metadata: {labels: {app: %{app}}} spec: containers: - name: web image: nginx:1.27 ports: [{containerPort: 80}] readinessProbe: httpGet: {path: /healthz, port: 80} periodSeconds: 3 --- apiVersion: v1 kind: Service metadata: {name: %{app}, namespace: %{ns}} spec: selector: {app: %{app}} ports: [{port: 80, targetPort: 80}] EOF j -n %{ns} svc %{app} '{.metadata.uid} {.spec.selector} {.spec.ports[0].port} {.spec.ports[0].targetPort}' > "$PRIV/q72.svc" cli %{ns} probe $K -n %{ns} wait --for=condition=Ready pod/probe --timeout=240s >/dev/null # the Deployment's Pods are meant to stay un-Ready; wait until they at least run for _ in $(seq 60); do [ "$(j -n %{ns} pod -l app=%{app} '{.items[*].status.phase}')" = "Running Running" ] && break; sleep 2; done """, "checks": [ C("probe gets the nginx page from http://%{app}", 3, NET + r"""okr %{ns} probe http://%{app}"""), C("both replicas Ready, still with an HTTP readiness probe on port 80", 2, r""" eq "$(j -n %{ns} deploy %{app} '{.status.readyReplicas}')" 2 || exit 1 [ -n "$(j -n %{ns} deploy %{app} '{.spec.template.spec.containers[0].readinessProbe.httpGet.path}')" ] \ && eq "$(j -n %{ns} deploy %{app} '{.spec.template.spec.containers[0].readinessProbe.httpGet.port}')" 80"""), C("the Service is untouched", 1, NET + r""" okr %{ns} probe http://%{app} || exit 1 eq "$(j -n %{ns} svc %{app} '{.metadata.uid} {.spec.selector} {.spec.ports[0].port} {.spec.ports[0].targetPort}')" "$(cat "$PRIV/q72.svc")" """), C("code.txt says 404", 1, r"""eq "$(tr -d ' \r\n' < "$WS/q72/code.txt" 2>/dev/null)" 404"""), ], "solution": """ k -n %{ns} get pod -l app=%{app} # 0/1 Running: started, but never Ready k -n %{ns} get endpointslices -l kubernetes.io/service-name=%{app} # endpoints listed, ready: false k -n %{ns} describe pod -l app=%{app} | grep -i readiness # probe failed: HTTP statuscode: 404 echo 404 > {WS}/q72/code.txt k -n %{ns} edit deploy %{app} # readinessProbe.httpGet.path: /healthz -> / k -n %{ns} rollout status deploy %{app} k -n %{ns} exec probe -- wget -T 3 -qO- http://%{app} | head -3 # A Service only sends traffic to Ready Pods. Selector and ports were right all along: the Pods # were held out by their own probe. "Running but 0/1" plus a Service with no ready endpoints is # always a readiness question first. """, }, { "id": 73, "domain": "NET", "title": "A Service for a Pod it cannot select", "params": lambda r: {"ns": ns_pick(r, "bridge")}, "task": """ Pod `legacy` runs in Namespace `%{ns}-old` and serves on port 80. Clients in Namespace `%{ns}` must reach it as `http://legacy-gw`, but a Service selects Pods of its own Namespace only. In `%{ns}` create: 1. Service `legacy-gw` with **no selector**, port 80. 2. EndpointSlice `legacy-gw-1` that belongs to that Service and carries the IP address of Pod `legacy` with port 80. Pod `probe` in `%{ns}` is there to test with. """, "setup": PODS + r""" mkns %{ns}; mkns %{ns}-old $K -n %{ns}-old run legacy --image=nginx:1.27 --port=80 >/dev/null cli %{ns} probe ready %{ns}; ready %{ns}-old """, "checks": [ C("legacy-gw has no selector and serves port 80", 1, r""" [ -n "$(j -n %{ns} svc legacy-gw '{.metadata.name}')" ] && [ -z "$(j -n %{ns} svc legacy-gw '{.spec.selector}')" ] && eq "$(j -n %{ns} svc legacy-gw '{.spec.ports[0].port}')" 80"""), C("EndpointSlice legacy-gw-1 belongs to it and carries the Pod's IP and port 80", 2, r""" ip=$(j -n %{ns}-old pod legacy '{.status.podIP}'); [ -n "$ip" ] || exit 1 eq "$(j -n %{ns} endpointslice legacy-gw-1 '{.metadata.labels.kubernetes\.io/service-name} {.addressType} {.endpoints[0].addresses[0]} {.ports[0].port}')" "legacy-gw IPv4 $ip 80" """), C("probe gets the page from http://legacy-gw", 3, NET + r""" okr %{ns} probe http://legacy-gw && $K -n %{ns} exec probe -- wget -T 3 -qO- http://legacy-gw 2>/dev/null | grep -qi nginx"""), ], "solution": """ IP=$(k -n %{ns}-old get pod legacy -o jsonpath='{.status.podIP}') cat </dev/null < /www/index.html && exec httpd -f -p 80 -h /www"] ports: [{containerPort: 80}] EOF done $K -n %{ns} create service clusterip %{app} --tcp=80:80 >/dev/null cli %{ns} probe for v in v1 v2; do $K -n %{ns} rollout status deploy %{app}-$v --timeout=240s >/dev/null; done ready %{ns} """, "checks": [ C("twelve requests to http://%{app} all answer v2", 3, r""" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do [ "$($K -n %{ns} exec probe -- wget -T 3 -qO- http://%{app} 2>/dev/null | tr -d ' \r\n')" = v2 ] || exit 1 done"""), C("both Deployments untouched and fully running", 2, r""" eq "$(j -n %{ns} svc %{app} '{.spec.selector.version}')" v2 || exit 1 for v in v1 v2; do eq "$(j -n %{ns} deploy %{app}-$v '{.metadata.generation} {.status.readyReplicas}')" "1 2" || exit 1; done"""), C("the Service selects app=%{app}, version=v2", 1, r""" eq "$(j -n %{ns} svc %{app} '{.spec.selector.app} {.spec.selector.version}')" "%{app} v2" """), C("response.txt says v2", 1, r"""eq "$(tr -d ' \r\n' < "$WS/q74/response.txt" 2>/dev/null)" v2"""), ], "solution": """ k -n %{ns} get pod --show-labels # app=%{app},version=v1 | v2 k -n %{ns} get svc %{app} -o wide # SELECTOR app=%{app} - matches both k -n %{ns} set selector svc %{app} app=%{app},version=v2 k -n %{ns} get endpointslices -l kubernetes.io/service-name=%{app} # two addresses left k -n %{ns} exec probe -- wget -T 3 -qO- http://%{app} > {WS}/q74/response.txt # v2 # `set selector` replaces the whole selector, so repeat the labels you keep. Narrowing or widening # a selector is how a Service does blue/green and canary without touching a Deployment. """, }, # =================================================================== 5.3 Ingress ==== { "id": 75, "domain": "NET", "title": "Ingress: two hosts and a default backend", "params": lambda r: {"ns": ns_pick(r, "edge"), "dom": r.choice(["riverbank", "harbor", "summit", "meadow"])}, "task": """ Namespace `%{ns}` has Services `shop` (port 80), `blog` (port 8080) and `fallback` (port 80). Create Ingress `%{dom}` with IngressClass `nginx`: - host `shop.%{dom}.test`, every path: Service `shop` on port 80, - host `blog.%{dom}.test`, every path: Service `blog` on port 8080, - any request that matches neither host goes to Service `fallback` on port 80. (No Ingress controller runs here; the object is graded.) """, "setup": NS + r""" mkns %{ns} $K -n %{ns} create service clusterip shop --tcp=80:80 >/dev/null $K -n %{ns} create service clusterip blog --tcp=8080:80 >/dev/null $K -n %{ns} create service clusterip fallback --tcp=80:80 >/dev/null """, "checks": [ C("Ingress %{dom} uses IngressClass nginx", 1, r"""eq "$(j -n %{ns} ingress %{dom} '{.spec.ingressClassName}')" nginx"""), C("shop.%{dom}.test / (Prefix) goes to shop:80", 2, ING + r""" ing %{ns} %{dom} ' import sys, json for r in json.load(sys.stdin)["spec"].get("rules", []): for p in r.get("http", {}).get("paths", []): b = p["backend"].get("service", {}) if r.get("host") == "shop.%{dom}.test" and p.get("path") == "/" and p.get("pathType") == "Prefix" and b.get("name") == "shop" and b.get("port", {}).get("number") == 80: sys.exit(0) sys.exit(1)'"""), C("blog.%{dom}.test / (Prefix) goes to blog:8080", 2, ING + r""" ing %{ns} %{dom} ' import sys, json for r in json.load(sys.stdin)["spec"].get("rules", []): for p in r.get("http", {}).get("paths", []): b = p["backend"].get("service", {}) if r.get("host") == "blog.%{dom}.test" and p.get("path") == "/" and p.get("pathType") == "Prefix" and b.get("name") == "blog" and b.get("port", {}).get("number") == 8080: sys.exit(0) sys.exit(1)'"""), C("the default backend is fallback:80", 2, r""" eq "$(j -n %{ns} ingress %{dom} '{.spec.defaultBackend.service.name} {.spec.defaultBackend.service.port.number}')" "fallback 80" """), ], "solution": """ k -n %{ns} create ingress %{dom} --class=nginx \\ --rule="shop.%{dom}.test/*=shop:80" \\ --rule="blog.%{dom}.test/*=blog:8080" \\ --default-backend=fallback:80 k -n %{ns} describe ingress %{dom} # In --rule the trailing * makes the path a Prefix; without it the path is Exact and only "/" # itself would match. The default backend is spec.defaultBackend, outside the rules: it takes # whatever no rule matched. Backend ports are Service ports (blog: 8080), not container ports. """, }, { "id": 76, "domain": "NET", "title": "Ingress: an exact path, a prefix, and a rewrite", "params": lambda r: {"ns": ns_pick(r, "gate"), "dom": r.choice(["tidewater", "northgate", "copperline", "fairlight"])}, "task": """ Namespace `%{ns}` has Services `status` (port 8080) and `app` (port 80). Create Ingress `%{dom}-routes` with IngressClass `nginx` for host `%{dom}.example.com`: - the path `/healthz` - **that path only**, nothing below it - goes to Service `status` on port 8080, - `/app` **and everything below it** goes to Service `app` on port 80, - the annotation `nginx.ingress.kubernetes.io/rewrite-target` is set to `/`. (No Ingress controller runs here; the object is graded.) """, "setup": NS + r""" mkns %{ns} $K -n %{ns} create service clusterip status --tcp=8080:80 >/dev/null $K -n %{ns} create service clusterip app --tcp=80:80 >/dev/null """, "checks": [ C("IngressClass nginx, one rule for %{dom}.example.com", 1, r""" eq "$(j -n %{ns} ingress %{dom}-routes '{.spec.ingressClassName} {.spec.rules[*].host}')" "nginx %{dom}.example.com" """), C("/healthz is Exact and goes to status:8080", 2, ING + r""" ing %{ns} %{dom}-routes ' import sys, json for r in json.load(sys.stdin)["spec"].get("rules", []): for p in r.get("http", {}).get("paths", []): b = p["backend"].get("service", {}) if p.get("path") == "/healthz" and p.get("pathType") == "Exact" and b.get("name") == "status" and b.get("port", {}).get("number") == 8080: sys.exit(0) sys.exit(1)'"""), C("/app is a Prefix and goes to app:80", 2, ING + r""" ing %{ns} %{dom}-routes ' import sys, json for r in json.load(sys.stdin)["spec"].get("rules", []): for p in r.get("http", {}).get("paths", []): b = p["backend"].get("service", {}) if p.get("path") == "/app" and p.get("pathType") == "Prefix" and b.get("name") == "app" and b.get("port", {}).get("number") == 80: sys.exit(0) sys.exit(1)'"""), C("rewrite-target annotation is /", 1, r""" eq "$(j -n %{ns} ingress %{dom}-routes '{.metadata.annotations.nginx\.ingress\.kubernetes\.io/rewrite-target}')" / """), ], "solution": """ k -n %{ns} create ingress %{dom}-routes --class=nginx \\ --rule="%{dom}.example.com/healthz=status:8080" \\ --rule="%{dom}.example.com/app*=app:80" \\ --annotation nginx.ingress.kubernetes.io/rewrite-target=/ k -n %{ns} get ingress %{dom}-routes -o yaml | grep -B1 -A1 pathType # No * : Exact - /healthz matches, /healthz/live does not. With * : Prefix - /app, /app/ and # /app/cart all match (by path element, so not /application). Annotations are how a controller's # own features are switched on; they mean nothing to any other controller. """, }, ]