Skip to content

feat: Carry internet egress to the node that serves it - #25

Draft
scotwells wants to merge 3 commits into
mainfrom
feat/vpcattachment-egress-conflist
Draft

scotwells wants to merge 3 commits into
mainfrom
feat/vpcattachment-egress-conflist

Conversation

@scotwells

@scotwells scotwells commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

A network declares whether its instances reach the internet, but nothing carries that decision to the machines running the workload. The node learns everything else about an interface from the configuration written when the interface is attached, and egress was missing from it.

This resolves which translating shard serves a network in this location and writes that into the same configuration, so a node installs an outbound route for a network that asked for one and none for a network that did not.

It also reports the address the network's traffic leaves from, with a note saying whether that address is shared with other networks and therefore unsafe to allow-list at a destination.

Architecture

sequenceDiagram
    participant NC as Network presence in this location
    participant C as Attachment controller
    participant P as Class parameters
    participant S as Egress shards
    participant N as Node
    participant A as Attachment status
    NC->>C: egress intent, resolved upstream
    C->>P: read what serves this class here
    C->>S: select by pool, location and address family
    C->>N: write the shard list into the interface config
    C->>A: report the source address and whether it is shared
    Note over C,A: no shard, or no address it can state, reports no egress
Loading

Test plan

  • A network with egress disabled produces the same configuration it did before
  • An address is reported only when the platform can state whether it is shared
  • Turning egress off withdraws the reported address
  • Two interfaces on one network in one location agree on the answer

Related to datum-cloud/network-services-operator#475

scotwells and others added 3 commits September 17, 2026 17:08
A network now reaches the internet from a cell when a consumer asked for
it. The egress intent projected onto NetworkContext.spec selects the
egress shards serving the class, and the shards' SRv6 SIDs are rendered
straight into the CNI conflist the node reads. Nothing new lands on
VPCAttachment: the node never reads it, and the conflist is written in
the same reconcile pass from the same inputs.

Intent is a function of (VPC, cell) alone. The kernel VRF is shared by
every attachment of a VPC on a node and the datapath route key has no
per-attachment component, so divergent intent between two attachments of
one VPC is undefined. Every attachment resolves the same NetworkContext
and computes the same ordered list, so the install is idempotent by
construction.

The egress block is absent whenever intent is Disabled, absent, or no
shard is bound, and a conflist rendered without it is byte-identical to
what the renderer produced before the field existed.

Key changes:
- Add EgressShardParameters, the cluster-scoped parameters type an
  InternetEgressClass names in its parametersRef for this controller to
  serve. It holds the shard namespace and a label selector over the
  network.datumapis.com/egress-* labels
- Select shards in NetworkInterfaceReconciler, requiring the IPv6 family
  label, in name order, skipping shards that report no SID
- Give galactic's BGPPlugin an omitempty egress block carrying an ordered
  shardSIDs candidate list the node keeps the first resolvable entry from
- Watch NetworkContext and EgressShard so intent and a shard's reported
  SID reach the conflist without waiting out a poll interval
- Pin go.datum.net/network and go.datum.net/network-services-operator to
  local checkouts, because both halves of the contract this reads are on
  unpushed branches. Lab only; it cannot merge in this state

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A consumer can now read the address their traffic leaves the platform on,
per interface. This controller is the only component that resolved which
shard a network bound to, so it publishes the answer on VPCAttachment
status, which NSO already reaches through the generic attachmentRef on a
network interface and reads without a typed dependency on this module.

Stability is derived here from the serving class's sharing rather than
left to the consumer: Shared reports None, Dedicated reports Network.
Sharing is an operator-side decision about the platform, and a consumer
interpreting it would be deciding for themselves whether allow-listing an
address is safe.

Nothing is published unless the platform can state it. No shard, absent
intent, Disabled, a shard that has reported no address, or a class whose
sharing never projected all report no egress at all. An absent address is
not an empty list and not a placeholder, and one that was published is
withdrawn when the path is.

Key changes:
- Add egress.internet.sourceAddresses to VPCAttachment status, keyed by
  family, with an IPv6-only family enum while IPv4 reach stays refused
- Resolve egress once per reconcile pass, so the conflist the node reads
  and the address the consumer reads are the same answer
- Report the preferred shard's address only. The candidate list is a
  preference the node resolves to one entry; reporting every candidate
  would name addresses the traffic does not leave on
- Keep whole-object Status().Update rather than moving to server-side
  apply. The new fields join a writer that already writes this status, so
  the writer count stays at two over disjoint field sets, and an update
  carrying a superseded resourceVersion is rejected rather than
  overwriting the other writer. A status half-written by SSA and half
  replaced wholesale is worse than either, so converting is a decision
  for the type, not a side effect of adding three fields
- Record that shards live in galactic-system, in the field an operator
  fills in. shardNamespace stays required: it names the nodes every
  network on a class leaves through
- Replace the committed replace directives with an untracked go.work.
  CI checks out this repo alone, so a committed replace to a sibling
  path fails its build outright

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI checks out this repo alone, so a module replace pointing at a local
path cannot resolve. Point both at the pushed commits instead, which CI
can fetch, so the build reflects the branches this depends on.

Key changes:
- Replace the network module with the pushed egress address commit
- Replace the operator module with the pushed egress API commit
- Drop the local workspace file in favour of resolvable versions

Revert both replaces once those modules release the fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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