CNPE — Cloud Native Platform Engineer
The request succeeded, so why is the app not ready?
Goal
On a real Crossplane 2.4, you create a namespaced App and verify a wrong readiness condition, team permissions, change convergence, and deletion delay. You do not presume success from a declaration or a single status display; you check the real Pod, HTTP, and the owner UID together.
Why it matters
Self-service is not work that automates only the create button. You must restrict the request's input and permissions, observe the created app, and be responsible for changes and deletion too. A new object with the same name or another team's response must not get mixed in as success of this request. You use k3s, Crossplane, a function, and a real container running inside a personal VM. These are not KWOK or fake Ready Pods. The installation can take several minutes. The estimated lab time is 55 minutes, and if you need more, extend the time before it expires. When the session ends, the VM and files are reclaimed. Download any records you need first, and do not apply anything to a production cluster outside the VM.
What is prepared and the tools
- API: App in platform.labhub.local/v1alpha1. Your own team is team-a, and the request name is parcel.
- Allowed input: replicas is 1–3, and channel is stable or preview. The developer account is system:serviceaccount:team-a:developer.
- Crossplane composes a Deployment and a Service, and the fixed Python app returns the channel and the responding Pod name.
- The stateless short GET app handles SIGTERM, and the termination grace period is 5 seconds. Do not apply this value as is to a database or a long-running job.
- team-b/sentinel is a preserved target that uses a separate healthy Composition. Keep it independent of the learner's configuration changes.
- Helper: python3 /opt/fixtures/cnpe_crossplane_lab.py followed by a command and a step number.
act applies the file for that step or performs the specified experiment. observe queries once. capture observes for up to 75 seconds without fixing the current configuration and then saves the specified JSON. grade does not modify files or change resources. If the state is still being prepared, do not reinstall; query the same run again. Previously verified observation files are preserved even if the same state disappears in later steps. Do not hand-edit a file into a success JSON.
Steps
- In /root/cnpe-api/initial.json, write apiVersion=platform.labhub.local/v1alpha1, kind=App, metadata.name=parcel, metadata.namespace=team-a, spec.replicas=1, and spec.channel=stable. act 1 applies this request with the developer account. Save accepted.json with capture 1, and check together a request that is Synced=True but Ready=False and the actual HTTP stable response.
- Copy /opt/fixtures/cnpe-crossplane-composition.json to /root/cnpe-api/composition.json. Change only the Ready in spec.pipeline[0].input.resources[0].readinessChecks[0].matchCondition.type to Available. Keep the remaining templates, selectors, image, and permissions. Apply it with act 2 and record the actual readiness in ready.json with capture 2.
- Query whether team-a's developer can create its own team's App but cannot create another team's App or a Deployment directly. With act 3, test the two actual permission rejections and the replicas=4 schema rejection. Save boundaries.json with capture 3. Do not use an administrator's success or a failed permission query as evidence of a rejection.
- In /root/cnpe-api/update.json, write a request for the same App, name, and team with replicas=2 and channel=preview. Apply it with act 4 and save updated.json with capture 4. Do not create a new request but keep the same UID, and check the current generation, updated replica count, available count, Pod ownership relationships, and the actual preview response.
- With act 5, perform the experiment that changes only the child Deployment to 3 replicas. This helper keeps the replica count, generation, and UID of the actual patch response. The App request stays at 2. With capture 5, save in reconciled.json the evidence that it returned to 2 in a generation later than that. Do not judge that you ran the experiment by looking only at the final number 2.
- With act 6, insert the educational finalizer labhub.io/handoff-hold and request deletion of parcel. Save deleting.json with capture 6. It is a state where a deletionTimestamp exists but the same UID can still be queried. Do not interpret this marker as guaranteeing the survival of all child resources.
- With act 7, remove only the one educational hold marker. Do not forcibly remove other finalizers. Save absent.json with capture 7 and check that the list queries for App, Deployment, ReplicaSet, Service, and Pod all succeed and the targets are gone. team-b/sentinel must keep the same UID and an actual healthy HTTP state.
- In /root/cnpe-api/diagnose.py, write diagnose(e). Following the diagnostic contract below, it returns a single string. Leave a missing required field, a number or string instead of a bool, an observation failure, and a state contradiction as unknown. This step is an independent coding task that does not recreate the earlier resources.
Step 8 diagnostic contract
The input e must have all six fields observed, accepted, synced, ready, deleting, and absent, and the values are real bools. This function receives a summary of the current state that has already been compared. accepted means the current request object exists.
- If there is a type or required field error or observed=False, it is unknown.
- If absent=True, it is absent only when accepted, synced, ready, and deleting are all False, otherwise unknown.
- If synced=True but accepted=False, or ready=True but synced=False, it is unknown.
- If there is no earlier contradiction and deleting=True, it is deleting when accepted=True, otherwise unknown.
- For the rest, if ready=True it is ready, next if synced=True it is synced, next if accepted=True it is accepted, and if all are False it is not_accepted.
absent is merely an absence confirmed by a normal query and does not mean a history of successful reclamation of a past request. Reclamation is checked separately in step 7.
Reference
Compare the App→Deployment→ReplicaSet→Pod ownership relationships with the currently responding Pod. Reject records of another object that merely has the same name. The observation files are material for learning the past state of the same lab run and are not a cryptographic remote attestation or an anti-cheating device. Grading from step 2 onward also checks the current Composition, and step 3 also checks the current XRD and permissions. For steps 4 and 5, if it is before deletion, it also checks the current app. After deletion, you need the actual reclamation evidence of step 7 to recheck the past records. Preparing a step does not overwrite existing files or later progress and fills in only the earlier steps it needs. Step 8 does not roll back the cluster state. A single educational finalizer does not preserve all child resources. Separating namespace permissions is not proof of network policy or quota as a whole. Official documentation: Composition · Finalizers.
Create the request and observe the readiness failure
In /root/cnpe-api/initial.json, write apiVersion=platform.labhub.local/v1alpha1, kind=App, metadata.name=parcel, metadata.namespace=team-a, spec.replicas=1, and spec.channel=stable. act 1 applies this request with the developer account. Save accepted.json with capture 1, and check together a request that is Synced=True but Ready=False and the actual HTTP stable response.
Request acceptance is not execution completion. Look separately at the App conditions and the HTTP response of the relevant Pod.
Restore a readiness condition that fits the app
Copy /opt/fixtures/cnpe-crossplane-composition.json to /root/cnpe-api/composition.json. Change only the Ready in spec.pipeline[0].input.resources[0].readinessChecks[0].matchCondition.type to Available. Keep the remaining templates, selectors, image, and permissions. Apply it with act 2 and record the actual readiness in ready.json with capture 2.
Query the condition name the Deployment actually provides. Removing the readiness check by setting it to None is not a recovery.
A real rejection test with a restricted account
Query whether team-a's developer can create its own team's App but cannot create another team's App or a Deployment directly. With act 3, test the two actual permission rejections and the replicas=4 schema rejection. Save boundaries.json with capture 3. Do not use an administrator's success or a failed permission query as evidence of a rejection.
Use --as=system:serviceaccount:team-a:developer with kubectl auth can-i. After the rejection, also confirm from the administrator's list that nothing was created.
Change the same request to a new channel
In /root/cnpe-api/update.json, write a request for the same App, name, and team with replicas=2 and channel=preview. Apply it with act 4 and save updated.json with capture 4. Do not create a new request but keep the same UID, and check the current generation, updated replica count, available count, Pod ownership relationships, and the actual preview response.
Look at metadata.generation and status.observedGeneration together. Old Pods that are terminating may remain.
Why a change to a child resource comes back
With act 5, perform the experiment that changes only the child Deployment to 3 replicas. This helper keeps the replica count, generation, and UID of the actual patch response. The App request stays at 2. With capture 5, save in reconciled.json the evidence that it returned to 2 in a generation later than that. Do not judge that you ran the experiment by looking only at the final number 2.
The source of a change you want to keep is the App. A manual change to a child resource can be overwritten at the next convergence.
Distinguish a deletion request from actual absence
With act 6, insert the educational finalizer labhub.io/handoff-hold and request deletion of parcel. Save deleting.json with capture 6. It is a state where a deletionTimestamp exists but the same UID can still be queried. Do not interpret this marker as guaranteeing the survival of all child resources.
The success of delete --wait=false is the success of the request. Query the finalizer and deletionTimestamp directly.
Confirm that only my resources were reclaimed
With act 7, remove only the one educational hold marker. Do not forcibly remove other finalizers. Save absent.json with capture 7 and check that the list queries for App, Deployment, ReplicaSet, Service, and Pod all succeed and the targets are gone. team-b/sentinel must keep the same UID and an actual healthy HTTP state.
A failed query and an empty list are different. An answer that removes my resources by deleting another team's as well is also a failure.
A diagnostic tool that leaves unknown states as unknown
In /root/cnpe-api/diagnose.py, write diagnose(e). Following the diagnostic contract below, it returns a single string. Leave a missing required field, a number or string instead of a bool, an observation failure, and a state contradiction as unknown. This step is an independent coding task that does not recreate the earlier resources.
First check the types, required fields, and whether it could be observed, and after ruling out contradictions, decide the deleting and ready states.