Repository navigation
Conversation
Contributor
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
thc1006
marked this pull request as draft
September 16, 2026 19:55
…hbound API
A provisioning request that could not be created was reported as progressing,
one that did not exist was reported as existing, and the northbound API was
never listening. The three are the same path, so they are fixed together.
The API calls go through one helper. Each of the four raw calls set no timeout
and no redirect policy, and each classified failures differently; a missing
token, a 403 and a 500 all became {"status": False}, which the reconciler's
`while status["status"]` then skipped, returning the "progressing" it had
initialised. Failures now carry what a caller has to decide on: whether another
attempt can help, and whether a write may have landed. Reading a token that is
not usable is a configuration failure of its own, and no request is sent.
The body is decoded once, after the status is known. Logging it first meant a
403 answered in HTML raised JSONDecodeError only when debug logging was on, and
a Kubernetes Status error body was read as a Cluster's nested conditions.
ProvisioningRequest is read by name. The lookup asked for the collection, so an
empty list came back without a "status" key and was reported as this request
progressing, whatever name was asked for. It is cluster-scoped, so no namespace
goes in the path, and a collection answered to a named request is a protocol
error rather than progress. The implicit PackageVariant lookup it carried
returned a bool where the caller indexed a mapping; PackageVariant ownership
belongs to the ensure path, which has the request to compare against.
A PackageVariant is only this request's if it says so. The name is chosen by
the request, so an existing one of the same name proved nothing; it is now
matched on the request uid and the cluster it targets, carried in annotations.
metadata.label was a string where Kubernetes wants a labels map. A create whose
answer was lost, or which lost the race, is settled by reading the resource
back rather than by assuming either outcome.
Reconciliation makes one observation and hands the retry back to kopf. The
handlers were async and ran synchronous HTTP and time.sleep, which blocks the
event loop kopf watches every resource with. The rendering budget counted
sleeps, so the time the requests themselves took did not count and a restart
was granted a fresh budget; it is now measured from the request's own creation
timestamp. The Ready condition is found by type: reading conditions[-1] meant
the same conditions in a different order decided differently.
Resource identifiers come from the objects they stand for. A fresh uuid4 on
every observation traced to nothing and changed each time it was read.
Business status is written before a handler stops. A kopf failure is recorded
in an annotation, not in the resource, so a creation that failed permanently
left the request's own status saying nothing. The cluster subhandler also
raised on the previous step's result, so a cluster that failed was recorded as
failed and then not raised for. kopf.PermanentError was constructed with event
kwargs and never raised.
The northbound API is served. threading.Thread(...).start was evaluated and not
called, so nothing listened and nothing said so; it is now bound in a startup
handler, so a port already in use stops the operator instead, and closed in a
cleanup handler. Flask's development server is not meant to serve this, so
waitress does.
Both route paths ended in a space, so neither was the path the SMO calls. A
collection GET returned from inside its loop, so only the first request was
ever answered, with a jsonify Response nested inside another dict that cannot
be serialised. Provisioning state was read a level above where it is recorded,
so every request read as PROGRESSING. Invalid input was answered 500. The
resource set was the string "test"/"sample". Requests are created with the
SMO's request id as their identity rather than their display name, which is
not unique, and an id that cannot be a Kubernetes object name is refused rather
than quietly rewritten into one the caller cannot use again.
The northbound API could not create anything anyway: the ClusterRole had no
create verb for provisioningrequests. status and finalizers are separate rules
now. The admission rule named apiGroups as "admissionregistration.k8s.io/v1",
which is not a group, so it granted nothing; no handler registers a webhook, so
it is removed rather than corrected into a permission that would then be real.
The image serves the probe: kopf only exposes it when --liveness is passed, and
get_current_timestamp called datetime.datetime.now on a module imported as
"from datetime import datetime", so it raised AttributeError.
tests/conftest.py puts controllers/ on the path the way the image does, so the
modules are imported the way they import each other and the tests exercise that
layout.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Five things an adversarial pass over the previous commit turned up. A retryable failure was reported as "progressing" with a message that said "Cluster instance rendering failed", so the state and the text a user reads beside it disagreed. The message now matches the state it is reported with. The start of the provisioning budget was parsed with one format string, and a timestamp in any other shape silently fell back to now, which hands out the fresh budget this is here to prevent. It is read with fromisoformat, which takes the trailing Z and any fractional seconds, and a value that still cannot be read says so rather than resetting the budget quietly. Every failure the API server answered a northbound create, list or read with became 502. An object it rejected is the caller's to fix and a missing permission is not, so 400, 409, 404 and 429 now carry through as themselves and only the rest are reported as an upstream fault. The identity check accepted names the API server does not: the pattern allowed a label starting with a hyphen and an empty label, and had no length limit, so "a.-b" was sent on and came back 422, which the mapping above would have reported as the server's fault. It is the real RFC 1123 subdomain now, label by label, bounded at 253. api_call closed its session before the answer was read. That works while the body is preloaded and stops working the moment it is not, so the answer is read inside the session. The test for stopping the northbound API asserted only that the module variable had been cleared, which stop_northbound does either way. It now asserts the thread has ended and the port can be bound again. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
kopf passes the handler a Spec, a mapping over the resource body, and Spec is
not a dict. The validator asked for one:
if not isinstance(params, dict):
return {"status": False, "reason": "... got Spec"}
so every real provisioning request failed validation and was recorded as
permanently failed, whatever it contained:
validate_cluster_creation_request(kopf.Spec(body))
-> {'status': False, 'reason': 'provisioning request must be an object, got Spec'}
Nothing caught it. The tests handed the handler dictionary literals, so they
agreed with the check rather than with the thing the check runs against. The
type checks are on Mapping now, and the operator tests build kopf.Body,
kopf.Spec, kopf.Patch and kopf.Memo and let configure fill the memo in, so the
handler runs on what it is actually given. Putting the dict check back fails
eight of the twelve.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…uests alone
Two things reading the CRD turned up.
The northbound API built its spec with data.get(), so a request that left out
name or description - and nothing requires either - was turned into a body
carrying nulls. The CRD types both as strings, so the API server rejects that,
and the caller is told its request was rejected over a field it was never asked
for. Checked against the CRD's own schema at
api@6f722d60f6757f4c5e9a7b60008d0eb3dc73d80e:
every field accepted
description: null rejected, "None is not of type 'string'"
name: null rejected, "None is not of type 'string'"
the key left out accepted
So the key is left out. Everything else the CRD declares matched already:
the resource is cluster-scoped, status is a subresource, and the field names
under provisionedResourceSet and provisioningStatus are the ones written here,
so none of it is pruned.
Fulfilled is where a provisioning request stops. Resume runs for every existing
request when the operator starts, and this drove a finished one from the
rendering step again: with its PackageVariant gone the request was re-rendered,
which re-creates it and reports the request as progressing after it had been
reported done. A fulfilled request is left alone.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
… empty spec.infrastructureRef on a Cluster API Cluster is an ObjectReference the user writes with an apiVersion, a kind and a name. Nothing in cluster-api sets its uid; the only mentions of InfrastructureRef.UID in that repository are in conversion tests. So reading the uid off it is right when a provider has set one and answers nothing the rest of the time, which is to say nearly always. The code and the pull request both described the identifiers as coming from the Cluster and its infrastructureRef, which reads as though the list carries something. It does not, and saying so is the difference between an empty set that is honest and one that looks like an oversight. Nothing behavioural changes here. The Cluster's own uid is still the node cluster id, and an inventory identifier this operator has not been given is still left out rather than invented. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Reading the reference this operator claims to serve, at api@6f722d60f6757f4c5e9a7b60008d0eb3dc73d80e, the answers did not match it. ProvisioningRequestInfo requires provisioningRequestData, provisioningRequestReference, status and provisionedResourceSet. provisioningRequestReference was not answered at all. It is "a unique reference assigned by the service producer at the time of request creation", which is the uid the API server assigned, not something invented here. provisioningRequestId belongs inside provisioningRequestData, where the reference declares it. It was answered one level up, where it is not declared, and left out of the place it is. ProvisionedResourceSet requires nodeClusterId and infrastructureResourceIds. The CR records the same two as oCloudNodeClusterId and oCloudInfrastructureResourceIds, and those were passed straight through, so neither required field was present under the name the API asks for. ProvisioningPhase is an enum: PENDING, PROGRESSING, FULFILLED, FAILED, DELETING. A recorded state with no mapping was answered as "UNKNOWN", which is not one of them. It answers PENDING now and logs the state it did not recognise, so the value is inside the enum and the surprise is still visible. Checked by validating what each route actually answers against the reference's own schemas: a named GET of a fulfilled request and of one with no status yet, a collection with items and empty, and a 201, all conform, and every phase the mapping can produce is in the enum. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…iption
The two disagree. The CRD types name and description as strings and so rejects
a null; the API reference requires both of every ProvisioningRequestData it
answers with. The previous commit left the keys out when the SMO gave neither,
which the API server accepts and the reference does not:
the SMO gave no description -> 'description' is a required property
the SMO gave no name -> 'name' is a required property
An empty string is what both take, and it says the SMO gave none rather than
inventing one, so that is what is stored and what is answered. Checked against
both schemas at once for every combination of the two fields, and for a
request written straight to the API server rather than through this API, which
nothing prevents and which may carry neither.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The deployment in nephio-project/catalog sets terminationGracePeriodSeconds: 5. Shutdown waited up to ten seconds for the northbound API to stop, so the wait was cut off by SIGKILL half way through and the line that says the server outlived its budget could never be reached. Three seconds, and settable, so a deployment that raises the grace period can raise this with it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The test Deployment overrode the pod to uid 0. The image builds for 65535 and
the catalog deployment runs as 65535, so the one place this is exercised before
release was the one place it ran as root, and a permission the operator's own
user lacks would have shown up only in a cluster.
It also declared no ports and no probes, so the liveness endpoint the image now
asks kopf to serve was served and never called. Both ports are named and the
probe points at the health one.
Checked in the built image, as the user it ships with:
uid=65535(o2ims) gid=65535(o2ims)
GET /healthz -> {"now": "2026-09-16T20:30:09.855647+00:00"}
Before this there was no endpoint to call: the CMD passed no --liveness, and
the probe handler raised AttributeError when it was reached.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The probe answered a timestamp, which says the process is running and nothing
about the thing this operator is here to serve. It reports the northbound API
too, so a served endpoint is distinguishable from a bound socket:
/healthz -> {"now": "...", "northbound": "serving"}
And the test deployment gains a readiness probe pointing at it. kopf answers
that endpoint only once it is running, so ready now means the operator started
and the API is still being served, rather than that a port was bound at some
point.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The test asserted that the timeout keyword reached the adapter. That says what
was asked for, not what happens, and it passes just as well against a client
that hangs.
A listening socket that accepts the connection and then writes nothing is what
a stalled API server looks like. The call gives up:
timeout=API_TIMEOUT the call raises after the read bound, 1.19s
timeout=None the call never returns; killed at 25s
The elapsed time is bounded below as well as above, because a connection
refused outright would also satisfy "did not hang" while exercising no timeout
at all.
The keyword assertion is kept alongside it. It says the policy is applied; this
says the policy works.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006
force-pushed
the
o2ims-provisioning-chain
branch
from
September 19, 2026 14:06
0a47c18 to
16c8b93
Compare
…e budget The northbound API has no authentication and acts with the operator's service account, but it listened on 0.0.0.0. Not declaring the port does not keep it off the pod network. It now listens on 127.0.0.1 unless NBI_HOST says otherwise, and kubectl port-forward reaches it under the caller's own RBAC. The probe reported "stopped" inside a 200, which no kubelet acts on. It now connects to the API and raises PermanentError, which kopf answers with 500. If the serving thread ends by itself, the operator exits, so the pod is restarted even where no probe is set. The budget was checked only after rendering had finished, so a package that never rendered kept the request progressing for good. It is checked while rendering too. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Member
Author
|
/test presubmit-nephio-o2ims-python-test |
Two things the reference turned up that the success paths hid.
ProblemDetails types status as a string. Every failure answered an object
there, so none of them was the shape this API documents. The message is the
status now; ProblemDetails declares no additionalProperties, so the time it
happened travels alongside it.
And load_incluster_config raises ConfigException, which is not an ApiException,
so no route caught it. Run anywhere the client cannot be configured, all three
answered Flask's HTML error page:
GET collection -> 500 <!doctype html> <title>500 Internal Server Error
GET named -> 500 <!doctype html> ...
POST -> 500 <!doctype html> ...
That is a northbound caller parsing JSON and getting a web page. It answers
503 and says the API server cannot be reached, which is the actionable part.
An errorhandler behind both of those answers the documented shape for anything
unforeseen, and tells an unauthenticated caller only that the request could not
be served; the traceback goes to the log, where it is of use. It re-raises
HTTPException, so werkzeug's own 404 for an unknown route stays a 404 rather
than becoming a 500.
Checked by validating every failure each route can answer against
ProblemDetails itself, including the ones that reach no cluster at all.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
nephio-project#1170 made every request to the API server carry a freshly read token. The northbound routes do not use that code: they reach the API server through a kubernetes client cached for the process, so the invariant rests on something this repository does not own. It holds. load_incluster_config installs a refresh hook by default, the client reaches it from auth_settings on every request, and it re-reads the token file once its own minute has passed. Verified by rotating the file and expiring the window, through auth_settings rather than by reading a field: first request bearer first-token rotated, window expired next request bearer second-token Nothing in the routes said so, and nothing would have failed if the client were built another way. Passing try_refresh_token=False is a one-word change, and it fails this now. certifi is declared for the tests, which was already being used as a CA bundle by the TLS corpus without being asked for. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The guard that stops a finished request being driven again reads the recorded status off the Body kopf hands the handler. Body is a mapping and not a dict, and asking for a dict is what made every real request fail validation one commit ago, so the one decision that keeps a fulfilled request alone is worth a test against the type it actually sees. Nothing changes in the operator. body["status"] is a plain dict today, so the check works either way; this fails if that stops being true. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1174. Closes #1177. Closes #1179.
A provisioning request that could not be created was reported as progressing, one that did not exist was reported as existing, and the northbound API was never listening. They are the same path, so they are here together.
What was happening
get_package_variantand the three other API calls flattened every failure into one shape:{"status": False, "reason": "..."}create_package_variantpassed that up, andcluster_creation_requestthen ran:which does not execute, so the function returned the
progressingit had initialised. A missing token, a 403 and a 500 all arrived that way. The measured path is a token file that is not there:create() with TOKEN naming a file that does not exist -> progressingcheck_o2ims_provisioning_requestasked for the collection:f"{KUBERNETES_BASE_URL}/apis/o2ims.provisioning.oran.org/v1alpha1/provisioningrequests"so a legitimately empty list came back with no
statuskey, took the "no status yet" branch, and was reported as this request progressing, whatever name was asked for. The requested name was never sent. It also carried an implicit PackageVariant lookup that returned a bool wheremanager.py:90indexed a mapping.threading.Thread(target=run_flask, daemon=True).startwas evaluated at import and never called, so nothing listened and nothing said so.What changes
One transport. Every API call carries a freshly read token, the trust settings #1170 resolved, a bounded timeout and no redirect. Failures are classified rather than collapsed: config, auth, not found, conflict, invalid, unavailable, protocol, transport, each carrying whether another attempt can help, and whether a write may have landed. A token that cannot be read is a configuration failure of its own and no request is sent.
The body is decoded once, after the status is known. Logging it first meant a 403 answered in HTML raised
JSONDecodeErroronly when debug logging was on, and a KubernetesStatuserror body was read as a Cluster's nested conditions.Named lookups. ProvisioningRequest is read by name, cluster-scoped so no namespace in the path, and a collection answered to a named request is a protocol error rather than progress.
PackageVariant ownership. The name is chosen by the request, so an existing one of that name proved nothing. It is matched on the request uid and the cluster it targets, carried in annotations.
metadata.labelwas a string where Kubernetes wants a labels map. A create whose answer was lost, or which lost a race, is settled by reading the resource back:One observation per pass. The handlers were
async defrunning synchronous HTTP andtime.sleep, which blocks the event loop Kopf watches every resource with. Each pass now makes one observation and hands the retry back to Kopf. The budget counted sleeps, so the time the requests themselves took did not count and a restart was granted a fresh one; it is measured from the request's owncreationTimestamp, which needs no schema change and survives a restart. The Ready condition is found by type:conditions[-1]meant the same conditions in a different order decided differently. The budget covers rendering too. It used to be checked only once rendering had finished, so a package that never rendered kept the request progressing for good.Resource identifiers come from the objects they stand for. A fresh
uuid4()on every observation traced to nothing and changed each time it was read; nothing traceable is an empty set rather than a placeholder.Business status before the handler stops. A Kopf failure is recorded in an annotation, not in the resource, so a creation that failed permanently left the request's own status saying nothing. The cluster subhandler also raised on the previous step's result, so a cluster that failed was recorded as failed and then not raised for.
kopf.PermanentErrorwas constructed with event kwargs and never raised.The northbound API is served. Bound in a startup handler, so a port already in use stops the operator instead of leaving it running with no API, and closed in a cleanup handler. Flask's development server is not meant to serve this, so waitress does. It listens on 127.0.0.1 unless
NBI_HOSTsays otherwise: it has no authentication and acts with the operator's service account, and leaving the port undeclared does not keep it off the pod network.kubectl port-forwardreaches it under the caller's own RBAC. If the serving thread ends by itself, the operator exits, so the pod is restarted even where no probe is set.Both route paths ended in a space. A collection GET returned from inside its loop, so only the first request was ever answered, with a
jsonifyResponse nested inside another dict that cannot be serialised. Provisioning state was read a level above where it is recorded, so every request read as PROGRESSING. Invalid input was answered 500. The resource set was the stringtest/sample. Requests are created with the SMO's request id as their identity rather than their display name, which is not unique, and an id that cannot be a Kubernetes object name is refused rather than quietly rewritten into one the caller cannot ask with again.What the caller is told. Every failure the API server answered a create, list or read with became 502, which says the server broke even when the request was the thing at fault. An object it rejected carries through as 400, a conflict as 409, a missing object as 404 and rate limiting as 503; only the rest are reported as an upstream fault. The identity check accepts what the API server accepts: the real RFC 1123 subdomain, label by label, bounded at 253, so an id like
a.-bis refused here rather than sent on and rejected there.The shape the reference describes.
ProvisioningRequestInforequiresprovisioningRequestData,provisioningRequestReference,statusandprovisionedResourceSet. The reference was not answered at all;provisioningRequestIdwas answered a level above where it is declared;ProvisionedResourceSetasks fornodeClusterIdandinfrastructureResourceIdswhile the CR records the same two asoCloudNodeClusterIdandoCloudInfrastructureResourceIds, and those were passed straight through, so neither required field was present under the name the API asks for; and a recorded state with no mapping was answered asUNKNOWN, which is not one of the five phases the enum allows.Failures answer the shape too.
ProblemDetailstypesstatusas a string and every failure answered an object there, so none of them was what this API documents; the message is the status now, and the time it happened travels alongside, whichProblemDetailsallows.load_incluster_configraisesConfigException, which is not anApiException, so no route caught it and all three answered Flask's HTML error page wherever the client cannot be configured — a northbound caller parsing JSON and getting a web page. That is a 503 naming the API server now, with an errorhandler behind it that answers the documented shape for anything unforeseen and tells an unauthenticated caller only that the request could not be served. It re-raisesHTTPException, so werkzeug's 404 for an unknown route stays a 404.The two specifications also disagree about
nameanddescription: the CRD types them as strings and rejects a null, the reference requires them of every answer. An empty string is what both take. Every route's answer is validated against the reference's own schemas, for a fulfilled request, one with no status yet, a collection with items and empty, a 201, and a request written straight to the API server rather than through this API.Permissions. The ClusterRole had no
createverb forprovisioningrequests, so the northbound API could not create anything anyway. status and finalizers are separate rules now. The admission rule namedapiGroupsasadmissionregistration.k8s.io/v1, which is not a group, so it granted nothing; no handler registers a webhook, so it is removed rather than corrected into a permission that would then be real and unused.The probe. Kopf only serves it when
--livenessis passed, andget_current_timestampcalleddatetime.datetime.nowon a module imported asfrom datetime import datetime, so it raisedAttributeError. Thenorthboundprobe connects to the API and raisesPermanentError, which kopf answers with 500. Answeringstoppedinside a 200 told a kubelet nothing, and the serving thread can stay alive after its listening socket is gone.Proof manifests
tests/conftest.pyputscontrollers/on the path the way the image does, so the modules are imported the way they import each other and that layout is exercised. #1034 and #1047 were both about it.163 of those passed before this change;
flake8 controllersreported 142 violations before it and none now.tox.inilistspy311as well aspy312, and the presubmit image carries only 3.12, so the suite was run on 3.11 too: 273 passed there as well.The northbound tests go through Flask's own routing and serialization, the operator tests through real
kopf.Body,kopf.Spec,kopf.Patch,kopf.Memoand realkopf.PermanentError/TemporaryError, and the listener is bound on a real socket. A route string, a status code, a nested Response object and a thread that was constructed but not started are none of them visible to a direct call of a view function. Passing dictionaries where kopf passes aSpecis how a validation check that rejected every real request went unnoticed.The read timeout is tested against a socket that accepts and then writes nothing, not against the keyword:
The elapsed time is bounded below as well, because a connection refused outright would satisfy "did not hang" while exercising no timeout at all.
The real image, the real
CMD, non-root:Those are the two ways serving can fail. The other is the thread ending, and
that one is verified through kopf's own event loop rather than by asserting
that a signal was sent:
So the cleanup handlers run and the process returns, rather than being killed
where it stands. The unit test covers this half — the signal is sent — and
that run covers kopf's half, that it is acted on.
That run was against an envtest kube-apiserver, with a service account token mounted where a pod has it, on the host network, so the ports there are 15000 and 18080.
The test deployment runs as the user the image ships with rather than overriding the pod to root, declares the health port, and has probes pointing at that endpoint. Reaching it says kopf started; a 500 says the northbound API does not accept connections.
and the endpoints, answered from inside that image:
What this does not do
The companion PR is nephio-project/catalog#149, a draft until this is merged and built. Reading
nephio/optional/o2ims/app/at28f531c:rbac.yamlhas the same gap this fixes in the test deployment:provisioningrequestscarries[get, list, watch, update, patch]and nocreate, so the northbound API cannot create anything. The inertadmissionregistration.k8s.io/v1rule is there too.--livenessendpoint this adds is served and never called. Update WatcherAgent to comply to NF CRDs in API repository #149 adds them.terminationGracePeriodSeconds: 5is what this operator now shuts down inside.2097228064147640320, which predates all of this and does not pass--liveness. Update WatcherAgent to comply to NF CRDs in API repository #149 has to move it to this PR's build before it can merge.Merging here does not update a running deployment. The northbound API stays on loopback until someone decides what authenticates the callers in front of it.
The node cluster id is the CAPI Cluster's own uid. The infrastructure resource ids are read off
spec.infrastructureRef, which Cluster API does not fill in, so that list is empty in practice until this operator is given a real inventory mapping; empty, rather than auuid4that changed on every observation and traced to nothing.An empty resource set is conformant:
ProvisionedResourceSetrequires the two fields, not values in them, so""and[]satisfy it. I had said this needed a correction in the api repository first; validating the answers against the reference's own schemas says otherwise, and that claim is withdrawn.PUTandDELETEon a named request are in the reference and are not implemented here.#1192 and #1193 are untouched and still open.
createTokenstill lists with the zero value, and the singleton is still published beforeStartfills it in.The second commit is an adversarial pass over the first. It found a retryable failure reported as progressing under a message that said it had failed, a start time parsed with one format string that quietly handed out a fresh budget in any other shape, the status mapping and identity check above, a session closed before its answer was read, and a stop test that asserted only what
stop_northboundsets either way rather than that the thread had ended.Release triage is yours.