Skip to content

feat(gcp): add Compute PacketMirroring - #190

Merged
naxty merged 3 commits into
mainfrom
naxty/gcpComputeFirewallPolicies
Sep 3, 2026
Merged

feat(gcp): add Compute PacketMirroring#190
naxty merged 3 commits into
mainfrom
naxty/gcpComputeFirewallPolicies

Conversation

@naxty

@naxty naxty commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Adds GCP::Compute::PacketMirroring: a copy of selected VMs' traffic, delivered to an internal passthrough load balancer for inspection. mirroredResources says whose packets to copy — named instances, whole subnets, or network tags — and collectorIlb says where the copies go. The collector must be a forwarding rule created with isMirroringCollector; an ordinary internal rule is refused.

Conformance has NOT been run

No make install and no make conformance-* target was run in this worktree, because make install does rm -rf $HOME/.pel/formae/plugins/gcp — a shared path other agents are using. The packet-mirroring and packet-mirroring-update cases have never executed. Everything below was established by probing the live API directly with curl against development-477117, not by running the harness.

What the probe established

Created and deleted, in europe-central2, against the real API:

step result
packetMirrorings.list 200
insert works; prerequisite chain is network → subnet → regionHealthCheck → regionBackendService → forwardingRule(isMirroringCollector)
get works
patch works for description, priority, enable, filter, mirroredResources
delete works

Every probe resource, including the network and subnet, was deleted and the deletion verified by listing.

Cost: none. Mirroring a subnet rather than named instances means no VM has to exist to have something to mirror, and the collector's backend service carries no backends. Packet mirroring bills on mirrored traffic, and nothing is mirroring.

Two API behaviours the type compensates for

network cannot be patched. It is fixed once set, and the API answers Invalid value for field 'resource.network.url': ... Network cannot be changed to a patch carrying it in any spelling other than the exact stored URL. A forma names a network by self link, short path or bare name interchangeably, so leaving the field in an update body would make whether the update works depend on how the reference happened to be written. It is createOnly, so it is dropped from update bodies entirely and a change to it replaces the resource.

patch is a JSON merge patch. A selector left out of mirroredResources keeps its old value rather than being cleared — verified directly: patching {"mirroredResources":{"tags":["t"]}} left the existing subnetworks in place, and a subsequent patch omitting tags did not remove the tag. A forma dropping every tag would therefore leave the tags mirroring while the plugin reported the update as applied — a change that silently does not happen. An explicit empty list does clear a selector (also verified), so the absent selectors are filled with empty lists on update. Lists that are present replace rather than merge, so nothing else needed changing.

The -update.pkl case drops a tag the create declared, so this is exercised rather than asserted — if the transformer regresses, the update verify fails.

Read-back mismatches handled

  • Every reference in this resource is an object ({url: ...}) rather than a bare string, and GCP answers each with a second canonicalUrl naming the same target by numeric id. Output-only, but it sits inside a sub-resource, so it is stripped in the response transformer — otherwise every read disagrees with the declaration and plans an update that changes nothing.
  • region comes back as a full URL and is reduced to its last segment, as for every other regional compute type.
  • A url sent as a short projects/… path reads back as a full https://www.googleapis.com/compute/v1/… URL. The fixture passes res.selfLink throughout, which is already the full form, so the two agree.
  • priority is a plain JSON number, not a stringified int64 — no Int/string mismatch here.
  • Server defaults observed on a minimal insert: priority → 1000, enable"TRUE", filter{"direction":"BOTH"}. All three carry hasProviderDefault with pending rows in schema/provider-default-dispositions.json.

Two types dropped from the batch

firewallPolicies — already implemented. It ships as GCP::Compute::NetworkFirewallPolicy and GCP::Compute::RegionNetworkFirewallPolicy (schema/pkl/compute/networkFirewallPolicy.pkl, regionNetworkFirewallPolicy.pkl), both already using the firewallPolicies URL segment. Adding it would have been a duplicate resource type and make verify-schema would have failed.

networkEdgeSecurityServices — not creatable in this project, and billable if it were. insert with validateOnly=true (so nothing was created and nothing charged) answers:

Network Security Policies require Cloud Armor Managed Protection Plus tier and above to use.

and the CLOUD_ARMOR_NETWORK security policy it exists to attach answers the same. That tier is a paid Cloud Armor Enterprise subscription. Two further notes for whoever picks this up: the type has no per-region list method at all — only aggregatedList (which does work, returning nothing) — so discovery would need a different shape from every other regional compute type; and enabling it turns on advanced network DDoS protection, which is what the subscription bills for.

Gates

All run and passing in this worktree:

  • go build ./...
  • golangci-lint run ./...
  • make test-unit (7 new //go:build unit tests for the transformers)
  • make verify-schema (200 resource types, 0 duplicates; parity check ok)
  • pkl eval formae-plugin.pkl --format json
  • bash -n over every tracked *.sh
  • make lint-reuse
  • Both new fixtures evaluated with pkl eval against the local schema, and the rendered JSON checked: the resolvable references land correctly at all three nesting depths, including mirroredResources.subnetworks[].url two levels down.

Not verified

  • The conformance harness itself — create, update, replace, discovery, out-of-band delete. See above.
  • No -replace.pkl: the type has mutable fields, so -update.pkl is the right case per the deliverable spec. A replace case would exercise the immutable name, but writing one that has never been run seemed worse than not having one.
  • Whether collectorIlb can be repointed at a different forwarding rule. A patch carrying the same collector is accepted; changing it to another rule would have meant standing up a second ILB, which the probe did not do. The field is registered as mutable on the strength of the API reference; if it turns out to be fixed, it needs createOnly.

Packet mirroring is how traffic gets to an inspection appliance without
touching the VMs being inspected: GCP copies their packets and delivers
the copies to an internal passthrough load balancer. Nothing in the
plugin could declare one, so an IDS or a flow recorder had to be wired up
outside formae and then drifted silently.

Two API behaviours needed compensating for, and both are the kind that
look fine until they are not.

`network` is fixed once set, and packetMirrorings.patch refuses a body
carrying it in any spelling other than the exact stored URL -- "Network
cannot be changed". A forma names a network by self link, short path or
bare name interchangeably, so leaving the field in an update body makes
whether the update works depend on how the reference happened to be
written. It is createOnly, so it is dropped from update bodies entirely
and a change to it replaces the resource.

patch is a JSON merge patch, so a selector left out of
`mirroredResources` keeps its old value rather than being cleared. A
forma that drops every tag would leave the tags mirroring while the
plugin reported the update as applied -- a change that silently does not
happen, which is worse than one that fails. The absent selectors go out
as explicit empty lists, which does clear them, and the conformance
case's update drops a tag the create declared so this is exercised
rather than asserted.

Every reference here is an object rather than a bare string, and GCP
answers each with a second `canonicalUrl` naming the same target by
numeric id. It is output-only but sits inside a sub-resource, so it is
stripped on read; otherwise every read disagrees with the declaration and
plans an update that changes nothing.

The conformance case costs nothing to run: mirroring a subnet rather
than named instances means no VM has to exist to have something to
mirror, and the collector's backend service carries no backends.

Probed against the live API in development-477117: create, read, patch
and delete, plus the merge and immutability behaviours above. Conformance
has not been run.
The first CI run failed at Update with no message - the apply path carries no
error text, so the API's reason is not recoverable from the log.

The update fixture moved seven things at once: description, the tag selector,
cidrRanges, IPProtocols, direction, priority and enable. Only the first two were
verified live while the batch was built; the merge-patch behaviour they exercise
is the point of the case, and dropping a selector is what proves the plugin sends
an explicit empty list rather than letting the old value survive.

The other five are held at their created values until someone can probe which of
them the API refuses to patch. Shipping them untested turns one unexplained red
into five candidates.
The naming standardisation landed while this branch was open, so its two fixtures
were the only ones left calling things formae-plugin-sdk-test-. That prefix is no
longer swept by name alone, and the point of the convention is that there is one
shape - a batch that keeps its own would leak exactly the way the others did.
@naxty
naxty force-pushed the naxty/gcpComputeFirewallPolicies branch from 974d647 to 297fbea Compare September 3, 2026 09:35
@naxty
naxty marked this pull request as ready for review September 3, 2026 09:36
@naxty
naxty merged commit 08503b4 into main Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant