Mock exam 2

A second paper: fifteen tasks, 120 minutes, 100 points, weighted like the real blueprint (15/25/25/20/15), and no task repeated from mock exam 1 (different competencies, same domains). Layers required: make full, nothing else staged. Task 5 pulls one public Helm chart, so the cluster needs its normal internet access; everything else is local. Score and clock are stored separately from exam 1's.

Lab layers
make full
Tasks
15
Target
70+ / 100
Your score
0/ 100

The clock

persists across reloads
time remaining
120:00
Tick a task's dot when its grading command passes. Score updates above.

Same rules as paper 1: start the timer, no peeking at curriculum sections or the answers below, and grade only at the end with the commands in the grading section. Docs and --help are allowed, as on the real exam. Anything a grading command cannot see does not count. Target: 70+ and no domain at zero. If a task stalls, note the time, move on, come back.

Tasks

click a task to collapse it · click the dot to score it

Create StorageClass mock2-retain on the local-path provisioner (rancher.io/local-path) with volumeBindingMode: WaitForFirstConsumer and reclaimPolicy: Retain. In default, create a 1Gi PVC mock2-data using it and a pod mock2-pod (image busybox:1.37, sleeping) that mounts it. The claim must end Bound and its PV must carry the Retain policy.

Create namespace mock2-net containing: deployment+service web (image ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine, port 8080, 1 replica) and two idle busybox:1.37 pods, trusted (label access=web) and other. Default-deny all ingress and egress in the namespace, then open exactly enough that trusted can fetch http://web:8080 by service name while other cannot reach it at all.

Create an Argo CD Application mock2-demo from the already-registered platform repo (http://gitea.lab:3000/lab/platform.git), path demo-app/base, deploying into namespace mock2-argo (have Argo CD create it), with automated sync, prune, and self-heal. Prove the self-heal: scale the live deployment to 5 by hand and watch Argo CD put it back to 2 with no git change and no further kubectl from you.

Author Task mock2-stamp that emits a result stamp whose value starts with mock2-, and Task mock2-echo that takes a param msg and prints it. Wire them into Pipeline mock2-relay (in default) as pipeline tasks named stamp and echo, where echo runs after stamp and receives the result as its param. Run it; the PipelineRun must Succeed and the echo step's log must show the stamp.

Using Flux, create a HelmRepository for https://stefanprodan.github.io/podinfo and a HelmRelease mock2-podinfo (in flux-system) installing chart podinfo into namespace mock2-helm (create it, or let Flux) with 2 replicas via values. The release must reach Ready with both replicas serving.

Using the CloudNativePG operator already installed, provision a single-instance Postgres cluster mock2-pg in default (1Gi storage; keep resources small). Wait until the operator reports it healthy, then write the auto-generated application username from its connection Secret to /tmp/mock2-pg-user.txt.

Write a kro ResourceGraphDefinition mock2space defining kind Mock2Space (schema: team string, cpu string defaulting to "1") that expands to a Namespace named after the team plus a ResourceQuota tenant-quota inside it capping requests.cpu at the given value. Create instance green (team mock2-green, cpu "2") and make sure the namespace and quota really exist.

Create WorkflowTemplate mock2-greeter in default with an input parameter who (default nobody) whose single step prints hello <who>. Submit Workflow mock2-hello referencing the template and overriding the parameter to platform. It must reach Succeeded with hello platform in its log. (Run it under a ServiceAccount carrying the executor rights the curriculum warns about, or it will fail no matter how correct the template is.)

Create a PrometheusRule that records namespace:pods_running:count: the number of Running pods per namespace, from kube_pod_status_phase. Prove Prometheus is evaluating it by querying the new series name through the API and getting results back.

Using LogQL against the lab's Loki, save every log line from namespace argocd in the last hour that mentions mock2-demo (task 3's Application; its self-heal left traces in the application controller's log) to /tmp/mock2-logs.txt, one line per line, no timestamps or JSON wrapping. The file must be non-empty and every line must contain mock2-demo. (Skipped task 3? Any Application name you did deploy works, but grading greps for mock2-demo.)

The kube-prometheus-stack ships a permanently firing Watchdog alert. Without editing any rule, create a 2-hour Alertmanager silence for alertname=Watchdog with a comment containing mock2, and confirm the alert now shows as suppressed. (Expire the silence after grading; it's a dead-man's-switch alert and something upstream may be watching it.)

Using the policy engine already installed, make the cluster generate a default-deny NetworkPolicy named default-deny in every newly created namespace whose name starts with mock2-gen, and only those, so the other exam namespaces stay untouched. Prove both halves: a matching namespace gets the policy, a non-matching one does not.

Produce Secret mock2-token in team-b with key token = s3cr3t. But the only thing you apply to the cluster is its sealed form: seal it for this cluster's controller with kubeseal, apply the SealedSecret, and let the controller materialize the real Secret.

Create namespace mock2-restricted enforcing the restricted Pod Security Standard. Run pod hardened there (image ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine) with a securityContext that passes the profile; it must reach Running. A bare kubectl run in that namespace must be rejected.

The Trivy Operator maintains a CIS compliance rollup for this cluster. Write its current fail count (taken from the report object's status, not from any dashboard) to /tmp/mock2-cis.txt, nothing else in the file. (On a fresh cluster the rollup's status stays empty until its cron first fires, hours away; getting the operator to fill it in now is part of the task.)

Grading

grade partial credit strictly

Run all of it; each line prints what earns the points. From the repo, make grade EXAM=2 runs this block verbatim: it extracts it from this very page, so paper and grader cannot drift.

# 1
kubectl get sc mock2-retain -o jsonpath='{.volumeBindingMode}/{.reclaimPolicy}{"\n"}'                # WaitForFirstConsumer/Retain
kubectl get pvc mock2-data -o jsonpath='{.status.phase}{"\n"}'                                       # Bound
kubectl get pv "$(kubectl get pvc mock2-data -o jsonpath='{.spec.volumeName}')" \
  -o jsonpath='{.spec.persistentVolumeReclaimPolicy}{"\n"}'                                          # Retain
# 2
kubectl -n mock2-net exec trusted -- wget -qO- -T 3 http://web:8080 >/dev/null && echo trusted-reaches-web
kubectl -n mock2-net exec other -- wget -qO- -T 3 http://web:8080 >/dev/null 2>&1 || echo other-blocked
kubectl -n mock2-net get netpol --no-headers | wc -l                                                 # at least a deny + your allows
# 3
kubectl -n argocd get app mock2-demo -o jsonpath='{.status.sync.status}/{.status.health.status}{"\n"}'   # Synced/Healthy
kubectl -n argocd get app mock2-demo -o jsonpath='{.spec.syncPolicy.automated.selfHeal}{"\n"}'       # true
kubectl -n mock2-argo scale deploy demo --replicas=5 && sleep 25 && \
kubectl -n mock2-argo get deploy demo -o jsonpath='{.spec.replicas}{"\n"}'                           # 2 (Argo CD put it back)
# 4
kubectl get pipelinerun -l tekton.dev/pipeline=mock2-relay \
  -o jsonpath='{.items[*].status.conditions[?(@.type=="Succeeded")].status}' | grep -o True | head -1   # True (a run Succeeded; failed retries don't hurt)
kubectl logs -l tekton.dev/pipelineTask=echo | grep mock2-                                           # the stamp crossed the task boundary
# 5
flux get helmrelease mock2-podinfo -n flux-system                                                    # Ready True
kubectl -n mock2-helm get deploy mock2-podinfo -o jsonpath='{.status.readyReplicas}{"\n"}'           # 2
# 6
kubectl get clusters.postgresql.cnpg.io mock2-pg -o jsonpath='{.status.phase}{"\n"}'                 # Cluster in healthy state
kubectl get clusters.postgresql.cnpg.io mock2-pg -o jsonpath='{.status.readyInstances}{"\n"}'        # 1
[ "$(cat /tmp/mock2-pg-user.txt)" = "$(kubectl get secret mock2-pg-app -o jsonpath='{.data.username}' | base64 -d)" ] && echo user-matches
# 7
kubectl get rgd mock2space -o jsonpath='{.status.state}{"\n"}'                                       # Active
kubectl get mock2space green -o name                                                                 # the instance exists
kubectl get ns mock2-green -o jsonpath='{.status.phase}{"\n"}'                                       # Active
kubectl -n mock2-green get resourcequota tenant-quota -o jsonpath='{.spec.hard.requests\.cpu}{"\n"}' # 2
# 8
kubectl get workflowtemplate mock2-greeter -o name                                                   # the template exists
kubectl get wf mock2-hello -o jsonpath='{.status.phase}{"\n"}'                                       # Succeeded
kubectl logs -l workflows.argoproj.io/workflow=mock2-hello -c main | grep 'hello platform'           # the override landed
# 9
kubectl -n monitoring port-forward svc/prometheus-kube-prometheus-prometheus 19090:9090 & sleep 3
curl -s 'http://localhost:19090/api/v1/query?query=namespace:pods_running:count' | jq '.data.result | length'   # > 0: recording
# 10
test -s /tmp/mock2-logs.txt && echo non-empty
test "$(grep -c mock2-demo /tmp/mock2-logs.txt)" = "$(wc -l < /tmp/mock2-logs.txt | tr -d ' ')" && echo every-line-matches
# 11
kubectl -n monitoring port-forward svc/prometheus-kube-prometheus-alertmanager 19093:9093 & sleep 3
curl -s http://localhost:19093/api/v2/silences | jq -r '.[] | select(.status.state=="active") | .matchers[].value'   # Watchdog
curl -s 'http://localhost:19093/api/v2/alerts?filter=alertname%3D%22Watchdog%22' | jq -r '.[0].status.state'         # suppressed
# 12
kubectl create ns mock2-gen-1 --dry-run=client -o yaml | kubectl apply -f - ; sleep 5; kubectl -n mock2-gen-1 get netpol default-deny -o name   # generated
kubectl create ns mock2-plain --dry-run=client -o yaml | kubectl apply -f - ; sleep 5; kubectl -n mock2-plain get netpol --no-headers | wc -l   # 0: the scope held
# 13
kubectl -n team-b get sealedsecret mock2-token -o name                                               # the git-safe form
kubectl -n team-b get secret mock2-token -o jsonpath='{.data.token}' | base64 -d; echo               # s3cr3t (controller unsealed it)
# 14
kubectl get ns mock2-restricted -o jsonpath='{.metadata.labels.pod-security\.kubernetes\.io/enforce}{"\n"}'   # restricted
kubectl -n mock2-restricted get pod hardened -o jsonpath='{.status.phase}{"\n"}'                     # Running
kubectl -n mock2-restricted run naked --image=busybox:1.37 --restart=Never -- sleep 5 2>&1 | grep -ci 'violate'   # >=1 (rejected)
# 15
[ "$(cat /tmp/mock2-cis.txt)" = "$(kubectl get "$(kubectl get clustercompliancereport -o name | grep -m1 cis)" \
  -o jsonpath='{.status.summary.failCount}')" ] && echo fail-count-matches                          # the CIS report carries its benchmark version in its name

Answers

open only after grading

Key moves only; each maps to a curriculum section where the full treatment lives.

1 · storage class, claim, binding

Section 1.3's pattern. The trap is expecting the PVC to bind before the pod exists: WaitForFirstConsumer means Pending-with-no-events is correct until mock2-pod mounts it. Retain goes on the class; the PV inherits it at provision time.

2 · NetworkPolicy isolation

examples/multitenancy/team-a.yaml is the pattern (section 1.1/1.4): a default-deny for the namespace, an egress DNS allow to kube-dns, then a pair of policies: egress from access=web pods to the web pods on 8080, ingress to web from access=web. Both ends must open; forgetting DNS makes even trusted fail on name resolution.

3 · self-healing Application

Section 2.2: an Application with source repoURL/path, destination.namespace: mock2-argo, syncPolicy.automated: {prune: true, selfHeal: true} and syncOptions: [CreateNamespace=true]. The repo is already registered (the lab did it), so no credentials work is needed. Self-heal reverts the manual scale within seconds; the deployment's replicas: 2 comes back from git.

4 · Tekton results

Section 2.4: mock2-stamp writes to $(results.stamp.path); the Pipeline's echo task gets params: [{name: msg, value: $(tasks.stamp.results.stamp)}] (the reference names the pipeline task, not the Task) plus runAfter: [stamp], though the result reference alone already orders them. If the run pends forever, you referenced a result that is never produced; tkn pr describe says so.

5 · Flux HelmRelease

Section 2.3: flux create source helm podinfo --url=https://stefanprodan.github.io/podinfo, then flux create helmrelease mock2-podinfo --source=HelmRepository/podinfo --chart=podinfo --target-namespace=mock2-helm --create-target-namespace --release-name=mock2-podinfo --interval=5m with --values (or a spec) setting replicaCount: 2. The --release-name is the catch: when targetNamespace is set and no release name is given, Flux composes one as <targetNamespace>-<name>, the chart names the deployment after it, and grading finds mock2-helm-mock2-podinfo instead of mock2-podinfo.

6 · CloudNativePG

Section 3.3. A minimal Cluster (postgresql.cnpg.io/v1): instances: 1, storage: {size: 1Gi}. Watch kubectl get clusters.postgresql.cnpg.io -w until phase "Cluster in healthy state". The operator creates mock2-pg-app; jsonpath='{.data.username}' | base64 -dapp. (In default deliberately: examples/crossplane/pg-cluster.yaml documents why a default-deny tenant namespace hangs initdb forever.)

7 · kro RGD

Section 3.6's tenantspace example, renamed: schema kind Mock2Space with team: string, cpu: string | default="1"; resources ns (Namespace named ${schema.spec.team}) and quota (in ${ns.metadata.name}, requests.cpu: ${schema.spec.cpu}). Mind the flow-style YAML trap the section calls out: ${…} in flow style needs quoting. Instance: kind Mock2Space, name green, spec: {team: mock2-green, cpu: "2"}.

8 · WorkflowTemplate + parameter

Section 3.4: a WorkflowTemplate whose spec.arguments.parameters declares who with a default, one template echoing {{workflow.parameters.who}}; the Workflow uses workflowTemplateRef: {name: mock2-greeter} and overrides via its own arguments.parameters. The SA needs create/patch on workflowtaskresults.argoproj.io: the same executor trap as always, stated in the task because the graded skill here is the template, not the RBAC.

9 · recording rule

Section 4.2: a PrometheusRule in monitoring with groups[].rules[]: {record: "namespace:pods_running:count", expr: 'count by (namespace) (kube_pod_status_phase{phase="Running"} == 1)'}. This lab's Prometheus selects all rules (ruleSelectorNilUsesHelmValues=false); on a stock install you'd need the release label (same story as ServiceMonitors). Give it a minute or two before querying: the operator has to reload rule files and the group has to complete an evaluation; measured here, the series first answered after ~90s.

10 · LogQL extraction

Section 4.3: kubectl -n monitoring port-forward svc/loki 3100:3100, then curl -sG http://localhost:3100/loki/api/v1/query_range --data-urlencode 'query={namespace="argocd"} |= "mock2-demo"' --data-urlencode 'limit=1000' | jq -r '.data.result[].values[][1] | rtrimstr("\n")' into the file (query_range defaults to the last hour, which is exactly the window asked for). The rtrimstr is not decoration: Alloy stores each line newline-terminated, so without it every value prints with a blank line after it and the every-line-matches check fails. logcli works too if you have it.

11 · Alertmanager silence

Section 4.2. The alertmanager pod carries amtool: kubectl -n monitoring exec alertmanager-prometheus-kube-prometheus-alertmanager-0 -- amtool silence add alertname=Watchdog --duration=2h --comment=mock2 --author=you --alertmanager.url=http://localhost:9093. Or POST the JSON to /api/v2/silences through a port-forward. A silenced alert keeps firing in Prometheus; Alertmanager suppresses the notification, which is the whole design point (and why the grading reads suppressed, not resolved). Expire it with amtool silence expire <id> afterwards.

12 · generate policy

Section 5.2: generation lives in Kyverno's classic dialect, a ClusterPolicy with a generate rule (generate.kind: NetworkPolicy, name: default-deny, namespace: "{{request.object.metadata.name}}", a podSelector-empty deny in data), matching kind Namespace with names: ["mock2-gen*"]. No extra RBAC is needed here: the chart's kyverno:background-controller ClusterRole already carries create/update/delete on networkpolicies, and generation lands within seconds of the namespace. The aggregated-ClusterRole move (label rbac.kyverno.io/aggregate-to-background-controller: "true") is only for generating kinds outside that default list.

13 · sealed secret

Section 5.1 verbatim: kubectl -n team-b create secret generic mock2-token --from-literal=token=s3cr3t --dry-run=client -o yaml | kubeseal --controller-namespace kube-system --controller-name sealed-secrets -o yaml, apply the output. The two controller flags are the trap: kubeseal's defaults point at a controller name this lab's chart didn't use.

14 · restricted PSS

Section 5.3. The namespace label: pod-security.kubernetes.io/enforce: restricted. The pod needs runAsNonRoot: true, seccompProfile: {type: RuntimeDefault}, and per-container allowPrivilegeEscalation: false + capabilities: {drop: ["ALL"]}; the demo-app base deployment in examples/demo-app/ is a working reference. The rejection message on the naked pod names every missing field: read it once and you can write the securityContext from memory.

15 · compliance report

Section 5.4, with two live traps. The report is not literally named cis: kubectl get clustercompliancereport lists k8s-cis-1.23: the benchmark version rides in the name, so discover it, don't guess it. And on a fresh cluster its status.summary is empty until the 6-hour cron fires; any spec change forces an immediate reconcile: kubectl patch clustercompliancereport k8s-cis-1.23 --type merge -p '{"spec":{"cron":"* * * * *"}}' filled the summary within a second here (patch the cron back after). Then -o jsonpath='{.status.summary.failCount}' > /tmp/mock2-cis.txt. The point stands: compliance lives in queryable objects with a status, like everything else on the platform.

After scoring

two papers, one picture

Put this score next to your mock exam 1 domains: a domain weak on both papers is a real gap, not a bad day, and its sections are where the next evenings go. The dashboard's weak-spots panel says the same thing from your drill history; when all three agree, believe them.

Clean up parents first: three of these namespaces have controllers standing behind them, and deleting a namespace out from under its controller just gets it rebuilt: measured here, Argo CD re-created mock2-argo fifteen seconds after the delete, Flux reinstalled mock2-helm seventy seconds after, and the kro instance still owns mock2-green.

# controllers first, or they rebuild what you are deleting
kubectl -n argocd delete app mock2-demo
flux -n flux-system delete helmrelease mock2-podinfo --silent
flux -n flux-system delete source helm podinfo --silent
kubectl delete mock2space green; kubectl delete rgd mock2space
# names below task text never fixed are yours; --ignore-not-found keeps the sweep going
kubectl delete clusterpolicy mock2-generate-default-deny --ignore-not-found
# now the namespaces stay gone
kubectl delete ns mock2-net mock2-argo mock2-helm mock2-green mock2-gen-1 mock2-plain mock2-restricted --ignore-not-found
# the default-namespace objects and the rest of the paper trail
kubectl delete pod mock2-pod; kubectl delete pvc mock2-data
kubectl delete pv "$(kubectl get pv -o jsonpath='{.items[?(@.spec.claimRef.name=="mock2-data")].metadata.name}')"   # Retain kept it, now Released
kubectl delete sc mock2-retain
kubectl delete cluster.postgresql.cnpg.io mock2-pg
kubectl delete pipelinerun -l tekton.dev/pipeline=mock2-relay
kubectl delete pipeline mock2-relay; kubectl delete task mock2-stamp mock2-echo
kubectl delete wf mock2-hello; kubectl delete workflowtemplate mock2-greeter
kubectl delete sa mock2-wf --ignore-not-found; kubectl delete role,rolebinding mock2-wf-taskresults --ignore-not-found
kubectl -n monitoring delete prometheusrule mock2-recording --ignore-not-found
kubectl -n team-b delete sealedsecret mock2-token          # its Secret goes with it
kubectl patch clustercompliancereport k8s-cis-1.23 --type merge -p '{"spec":{"cron":"0 */6 * * *"}}'   # if you sped it up
pkill -f 'port-forward.*1909[03]'                          # the two grading port-forwards, if you pasted by hand
# and let the Watchdog page someone again (both amtool calls need the url flag)
kubectl -n monitoring exec alertmanager-prometheus-kube-prometheus-alertmanager-0 -- sh -c \
  'am=http://localhost:9093; amtool silence expire $(amtool silence query -q alertname=Watchdog --alertmanager.url=$am) --alertmanager.url=$am'