Repository navigation
chore: gcp(cloud-run) - drop non-schema label fields, bump to .post4 - #21
Conversation
|
| Filename | Overview |
|---|---|
| tools/c7n_gcp/c7n_gcp/resources/cloudrun.py | Introduces schema-constrained, recursively sanitized replacement bodies and uses them for Service and Job label mutations. |
| tools/c7n_gcp/tests/test_cloudrun.py | Adds helper- and policy-level assertions covering outgoing body shape, nested annotations, copy semantics, unknown fields, and both label actions. |
| uv.lock | Updates only the editable project package versions to the new prerelease revision. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Listed Cloud Run resource] --> B[knative_body]
B --> C{Unknown root field?}
C -- Yes --> D[Raise PolicyExecutionError]
C -- No --> E[Keep apiVersion, kind, metadata, spec, status]
E --> F[Remove nested c7n-colon annotations]
F --> G[Merge metadata labels]
G --> H[replaceService or replaceJob]
Reviews (2): Last reviewed commit: "chore: bump version to post4" | Re-trigger Greptile
ajkerrigan
left a comment
There was a problem hiding this comment.
Thanks, looks good and the comments may help other folks avoid some of the "wait, can't we..." thoughts that I had while looking this over 😅
| Cloud Run replaces the whole object, so the body is the resource itself | ||
| rather than a fragment, and the api rejects the request with a 400 on any | ||
| field it can't place. A resource carries two kinds it can't: the top level | ||
| `labels` key that augment() flattens out of the metadata, and the | ||
| annotations the policy's filters leave behind - at the root, and nested | ||
| within `spec` when a list-item filter matched on something there. |
There was a problem hiding this comment.
Thanks - this is a helpful comment. The part that initially threw me here was that most other labeling operations specify something like updateMask='labels' and the error message in cloud-custodian#11034 had a URL with updateMask=metadata.labels in it but that option doesn't seem to be available to Python until v2 of this service.
The replaceService() docs do call out:
Only the spec and metadata labels and annotations are modifiable.
But "whole object replace" does still feel like the right way to think about it 👍
ENG-8580
what
CloudRunService.get_label_paramsandCloudRunJob.get_label_paramsbuild their request body by deepcopying the whole resource, because
replaceServiceandreplaceJobreplace the entire object ratherthan patching it. A shared
knative_body()helper now does thatcopy, keeping only the fields the Knative schema defines at the root
of a Service or a Job —
apiVersion,kind,metadata,specandstatus— and dropping c7n's own annotations wherever they sit,including nested inside
spec.A root field that is neither of those raises rather than being
dropped. An allow-list under a whole object replace means a field
it does not recognise would be deleted from the live service on a
200, so the one case it cannot handle safely is the one case it
refuses to guess at: the action fails, and nothing is written.
statusis still sent, andmetadata.resourceVersionis stillechoed back, both unchanged from before. The latter deserves its
own look: the list endpoint can hand back a resourceVersion that
lags a recent write by another actor, and replaying a stale one
draws a 409 that aborts the run for every resource after it. That
is tracked in
ENG-8590.
This fixes the body, not every failure on this path.
set-labelsand
mark-for-opongcp.cloud-run-jobstill fail, with a 404:replaceJobis served only by the regional Cloud Run endpoint,while the client is built against the global one. That is an
unrelated defect in the transport rather than the payload, and is
tracked in ENG-8589.
Every workspace package moves from
.post3to.post4viamake pkg-increment PKG_INCREMENT=post, which increments the root and thetwelve tool packages, regenerates
c7n/version.py, and updatesuv.lock.why
Every label mutation on
gcp.cloud-run-serviceandgcp.cloud-run-jobfailed:Two kinds of key reached the body.
augment()flattensmetadata.labelsto a synthetic top levellabelskey, which thelabel action needs in order to read the resource's current labels
but which the api has no field for. Separately, the annotations c7n
keeps on a resource rode along on the deepcopy —
c7n:MatchedFiltersfrom a plain value filter is enough to trigger the rejection. Both
set-labelsandmark-for-opwere affected, since both reach thesame
get_label_params.Annotations are not confined to the root. A
list-itemfilterannotates the objects it matched in place, so a policy filtering on
spec.template.spec.containersleavesc7n:MatchedFilterson acontainer, several levels inside a field the body has to carry
verbatim. Removing only the root keys would leave that policy shape
failing with the same error at a different path, so the copy drops
annotations at every depth.
Only the colon form is recognised below the root.
c7n.metricsisthe sole dotted key c7n writes and it is only ever set on the
resource itself, whereas kubernetes annotation keys — user
controlled, and carried in
metadata.annotations— may contain adot but never a colon. Matching the dotted form while descending
would strip an annotation such as
c7n.example.com/owner, andunder a whole object replace a key omitted from the body is a key
deleted from the service.
Keeping a whitelist of schema fields rather than removing the two
known offenders means neither a new annotation key nor another
synthetic key added by
augment()can reintroduce the failure.The version bump cuts the next prerelease, so the fix ships.
Fixes: cloud-custodian#11034
testing
New coverage asserts the outgoing request body directly, for both
resources and for both
set-labelsandmark-for-op: the bodycarries exactly the five schema fields and neither contaminant key,
metadata.labelsholds the merged label set, and the resourceitself keeps the synthetic
labelskey that the action reads itscurrent state from. The policy-level tests spy on the api call and
invoke it, so parameter names are still validated against the
discovery document and the body is still serialized. Helper-level
tests cover an annotation nested in a container inside
spec, adotted user annotation surviving at both annotation sites, the
resource being copied rather than mutated, and an unrecognised root
field raising while the expected synthetic keys pass through.
The expected field set is spelled out in the tests rather than
imported from the module under test, so that widening the module's
own tuple fails them instead of travelling through them.
The existing replay tests could not catch this — the replay
harness discards request bodies and returns the recorded response
regardless of what was sent, which is why the broken behaviour
shipped with passing tests.
mark-for-opis exercised withop: notify, because neitherresource registers a
stopordeleteaction for it to mark for.Verified against a live project, since no replay test can observe
this. With the original body the api returns the reported 400,
naming both
labelsandc7n:MatchedFiltersin one response;with the allow-list the same policy succeeds and the label lands on
the service. A
list-itempolicy reproduces the nested case —Unknown name "c7n:MatchedFilters" at 'service.spec.template.spec.containers[0]'— and succeeds onceannotations are dropped at depth. The equivalent job policy still
fails with the 404 above, unchanged by this work.
The replace semantics were measured rather than assumed. Omitting a
single user annotation from an otherwise unchanged body returns 200
and removes that annotation from the service, so a field left out of
the body really is a field deleted. Matching the dotted prefix while
descending was confirmed to delete
c7n.example.com/ownerfrom alive service on an ordinary
set-labelsrun, which is what thecolon-only match below the root prevents; with the colon-only match
a service carrying both
c7n.example.com/ownerand an unrelatedteam.example.com/oncallkeeps both across a run that lands itslabel.
docs
No docs needed. Bug fix with no schema or user-facing surface.
🤖 Generated with Claude Code