Skip to content

Use canonical locations for compute placement - #264

Merged
scotwells merged 16 commits into
mainfrom
feat/location-native-compute
Sep 10, 2026
Merged

scotwells merged 16 commits into
mainfrom
feat/location-native-compute

Conversation

@scotwells

@scotwells scotwells commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Compute placement now speaks the same language as the rest of Datum Cloud. A workload is placed at locations, the names you see in datumctl get locations, instead of airport-style city codes. There are two ways to say where a placement runs:

  • Name the locations. The default. The placement is pinned to exactly the locations you list.
  • Select them by topology. The advanced path. A label selector over each location's topology (city code, region, or any other key the platform projects) picks every Ready location that matches, and the placement follows the fleet as locations come and go.

The CLI, portal, API, samples, and docs all move together so a developer sees one concept from the first deploy to the instance detail page.

What a developer sees

Deploy by name. Tab completion offers the Ready locations projected into the project, and keeps working through a comma-separated list. --city completes the city codes those locations serve, and --location-selector completes the key=value topology pairs they carry, so the available topology keys can be discovered from the shell:

$ datumctl compute deploy api --image=ghcr.io/acme/api:1.4.2 --location=us-east-1,eu-west-1 --min=2 --port=8080

Resolving workload "api" in project acme-prod...
  Workload does not exist — creating.
  Placement "default": locations=[us-east-1, eu-west-1], min=2

  PLACEMENT  LOCATION    DESIRED  READY  PHASE
  default    us-east-1         2      2   Running
  default    eu-west-1         2      2   Running

The interactive prompt asks for locations and "min replicas per location" instead of cities.

Deploy by topology. --location-selector takes ordinary label selector syntax. Every Ready location that matches gets a deployment. A location that later starts matching is picked up on its own, and one that stops matching is torn down:

$ datumctl compute deploy api --image=ghcr.io/acme/api:1.4.2 --location-selector='topology.datum.net/city-code=DFW'
  Placement "default": selector=[topology.datum.net/city-code=DFW], min=1

$ datumctl compute deploy api --image=ghcr.io/acme/api:1.4.2 --location-selector='topology.datum.net/region in (us-east-1,eu-west-1)'

In a manifest the same placement is a standard selector block, and the workload's status reports what it resolved to:

spec:
  placements:
  - name: dfw
    locationSelector:
      matchLabels:
        topology.datum.net/city-code: DFW
    scaleSettings:
      minReplicas: 1
status:
  placements:
  - name: dfw
    locations:
    - name: gcp-us-south1-a
    - name: gcp-us-south1-b

A placement sets either locations or locationSelector, never both. An empty selector is rejected rather than treated as "everywhere". A placement that resolves to nothing stays visible in status with a NoMatchingLocations condition instead of silently disappearing.

City codes are a shortcut for a selector. --city=DFW,IAD is shorthand for a location selector on topology.datum.net/city-code, so the placement runs at every Ready location in those cities and keeps tracking them as locations are added or removed. The saved workload config records the selector, and a city with no Ready location is rejected at admission with the list of locations that do exist. Pin a placement to specific locations with --location instead.

Inspect with --location. Every list and describe command filters by location and shows a LOCATION column, and workloads describe shows a placement's selector alongside the locations it resolved to:

$ datumctl compute workloads --location=us-east-1
$ datumctl compute instances --location=us-east-1
$ datumctl compute instances describe api-us-east-1-0
  Placement      default
  Location       us-east-1

rollout, scale, restart, and destroy follow the same pattern.

Mistakes are caught at admission. An unknown location, a location that is not Ready or where compute is not available, a duplicate in one placement, or a selector that is empty, malformed, or matches no such location is rejected with a field-level error listing the valid choices, rather than landing in a deployment that never schedules.

Portal (consumer and provider) labels workload and instance views as "Locations", shows the location on instance cards and page chrome, reports replicas per location, and for selector-based placements shows the selector and the locations it resolved to.

API changes

This changes the alpha API, with a shim so workloads stored before the rollout keep running:

  • Workload.spec.placements[].cityCodes is replaced by placements[].locations (a list of {name} references) or placements[].locationSelector (a label selector over location topology). Exactly one must be set, enforced by CEL on the CRD and by the webhook.
  • cityCodes stays as a deprecated field so existing objects survive the CRD change. Admission rewrites a placement that only names city codes into the equivalent city-code selector on every write, and the workload controller rewrites each stored workload the first time it reconciles it after the rollout, writing it back before deriving deployments. Until that write succeeds a workload keeps what it already runs. The federator finalizes pre-location deployments by reading the city off their hub copy and removing the city-keyed propagation policies once nothing routed by that city remains.
  • Workload.status.placements[].locations lists the locations a placement currently resolves to.
  • WorkloadDeployment.spec.cityCode becomes spec.locationRef. The status.location field is removed since the location is now declared in spec, and kubectl get workloaddeployments shows a LOCATION column.
  • Instances carry a compute.datumapis.com/location label instead of city-code.
  • Deployments and cells route with the topology.datum.net/location label.

Placements are validated and resolved through the location source abstraction, so both the NetworkServices and Locations sources work. A location is placeable when it is Ready and compute is available there. Readiness comes from the Location (every LocationBinding counts as Ready; a Location must carry a true Ready condition). Availability comes from the ServiceAvailability records the platform mirrors into the project: a record for the compute service naming the location with Available=True. A control plane that does not serve that kind enforces no availability gate. The workload controller watches both the project's locations and its availability records, so placements follow changes without polling.

Samples (including a new location-selector-sandbox.yaml), generated API docs, the compute DX enhancement doc, and e2e fixtures are updated to match.

Validation

  • make test
  • consumer UI tsc --noEmit
  • provider UI tsc --noEmit

scotwells and others added 2 commits September 1, 2026 19:53
Resolve the location-native placement change against the location source
abstraction and runtime classes that landed on main.

- Placements validate by location name through locations.ListPlacementLocations,
  so both the NetworkServices and Locations sources keep working. A
  PlacementLocation now carries Ready, true for every LocationBinding and the
  Ready condition for a Location, and only Ready locations may be placed at.
- PropagationPolicies key on (location, runtime class) with names of the form
  location-<name>[-class-<class>], selecting on topology.datum.net/location.
- Location resolution and the ServingLocation watch stay behind the networking
  integration and the source-selected kind guard from main.
- Agent tooling, instance pod identity labels, and the provider fleet view read
  the location instead of a city code; the mismatch reason is LocationMismatch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@scotwells
scotwells requested a review from savme September 9, 2026 17:28
scotwells and others added 4 commits September 9, 2026 17:22
Naming locations stays the default. A placement may instead carry a
locationSelector, a label selector matched against the topology of the
project's Ready locations (city code, region, and any other key the platform
projects). Every matching location receives a deployment, and the set follows
the fleet: the workload controller watches the project's placement locations,
so a location that starts or stops matching is picked up or torn down without
polling.

- API: placements[].locationSelector, exactly one of locations or
  locationSelector (CEL and webhook); status.placements[].locations reports
  what each placement resolved to; NoMatchingLocations names a placement that
  resolves to nothing.
- Admission rejects an empty or malformed selector and one that matches no
  Ready location, mirroring how an unknown location name is rejected.
- CLI: deploy --location-selector, mutually exclusive with --location and
  --city; describe shows the selector and resolved locations.
- Portal: placements show the selector and the locations it resolved to.

The controller watches the project's placement locations so a selector
follows the fleet. The watch is the first thing to watch LocationBindings, so
the ClusterRole now grants it, and it is kept off any control plane that does
not serve the kind: an informer whose list is rejected never syncs and blocks
every controller on the manager. Test locations repeated across files are
named constants so the linter accepts them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
--city no longer resolves city codes to location names on the client. It
emits a locationSelector on topology.datum.net/city-code, so the placement
deploys to every Ready location in those cities and follows locations as
they are added or removed. Admission already rejects a city that matches no
Ready location, which replaces the client-side check, and
--all-matching-locations has no purpose left.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
--location and --city already completed, but only for the first element:
the shell matches candidates against the whole value typed so far, so
"--location=us-east-1,eu-" offered nothing. Candidates now carry the typed
elements in front of them, already-chosen elements are not repeated, and no
space is appended so a comma can follow.

deploy offers only Ready locations and their city codes, since admission
rejects anything else; list and describe filters keep offering every
projected location. --location-selector completes the key=value pairs found
in the topology of Ready locations, so the available topology keys can be
discovered from the shell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A location can be Ready for the platform generally yet have no compute cell
behind it. The platform records where each service runs as a
ServiceAvailability and mirrors those records into entitled projects, so
placement now reads them: a location is placeable when it is Ready and a
record for the compute service names it and reports Available. Admission,
selector resolution, placement status, and shell completion all share that
one predicate. A control plane that does not serve the kind enforces no
availability gate, so placement there behaves as before the mirror existed.

The workload controller watches the mirrored records, per control plane and
only where the kind is served, so a placement starts or stops running at a
location as compute availability there changes. The ClusterRole grants the
watch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@scotwells
scotwells force-pushed the feat/location-native-compute branch from e3e345b to 3a081cf Compare September 9, 2026 22:22
@scotwells
scotwells marked this pull request as ready for review September 9, 2026 22:48
Workloads stored before placement moved to locations still name city codes.
Pruning the field on rollout would leave their placements resolving to
nothing and tear down everything they run, so the field stays, deprecated,
and is rewritten into the selector --city now emits: every location whose
topology.datum.net/city-code is one of the codes.

Admission rewrites it on any write, so nothing new is stored with it. The
workload controller rewrites a stored workload the first time it reconciles
it after the rollout and writes it back before deriving deployments; until
that write succeeds the workload keeps what it runs. A placement that names
city codes beside locations or a selector is rejected rather than guessed
at.

Deployments from that era carry no location, so the federator reads the
city off their hub copy while finalizing them and removes the city-keyed
PropagationPolicies once no deployment routed by that city remains. The CLI
and portal show unmigrated placements by their cities in the meantime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ecv
ecv previously approved these changes Sep 10, 2026
scotwells and others added 4 commits September 9, 2026 19:33
Deploying a workload with an HTTP port now ends with a URL. Previously
--port opened a port and gave the developer nothing to point a browser at.

--http-port declares the workload an HTTP service and publishes it on a
Datum-managed HTTPS URL, backed by a NetworkService and an HTTPProxy that
load-balance across every city the workload runs in. --no-http removes it.

--port is removed and hard-errors rather than aliasing to --http-port:
silently aliasing would publish every existing workload on the next
upgrade.

Adds `compute open` (heroku/fly/railway convention) and a `compute
domains` group for listing domains, showing one workload's URL in detail
with per-city backend health, and attaching custom hostnames. Surfaces
the URL in `compute workloads` and includes it in `compute destroy`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deploy calls Declare on every run, so the common case is a workload that
already has a URL. Pin that a redeploy neither recreates it nor churns it
when nothing changed, that a changed port is written through, and that
the canonical hostname survives — reissuing it would silently break every
reference a developer has already shared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Advanced proxy configuration — custom hostnames, DNS verification, path
routing, certificates — belongs to dedicated ALB tooling, not the compute
plugin. This plugin publishes a URL and stops there.

Removes `compute domains` and `compute open`. The per-city backend health
view moves into `workloads describe`, which already exists and is where a
developer debugging their own workload looks; its health row is labelled
Serving so it reads distinctly from the workload's own Health line.

deploy still reads and carries forward the custom hostnames already on the
proxy, and still fails closed if it cannot read them. That matters more now,
not less: hostnames are configured out of band, so a redeploy that clobbered
them would break something this plugin never saw being set up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacking on the locations work leaves the URL views speaking the old
vocabulary. The per-location backend table said CITY, and its next-steps
line told users to run 'compute instances --city=', a flag that no longer
exists — the suggested command would have failed.

Renames url.Location.City to Location, the column to LOCATION, and the
suggested flag to --location, so every place a URL reports where it is
serving from uses one word for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scotwells and others added 2 commits September 9, 2026 19:45
CI's golangci-lint run fails on this branch: deployFromFlags crossed the
gocyclo limit at 32 once the locations validation and the HTTP service
handling both landed in it, and two test fixtures wrote "http" where the
httpPortName constant already exists.

Lifts the three mutually exclusive placement flags into
resolveLocationSelector, which is pure and now has the coverage the inline
version never could — everything else in deployFromFlags needs a control
plane behind the activation gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevwilliams
kevwilliams previously approved these changes Sep 10, 2026

@kevwilliams kevwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placement move from city codes to canonical locations is well handled end to end: the deprecated cityCodes field is rewritten into an equivalent locationSelector on admission (Default hook) and again by the workload controller on first reconcile after rollout, with writes gated through validation so a workload never loses its running deployments. The federator correctly recognizes deployments with no LocationRef as pre-migration, reads the city off the hub copy before deleting it, and cleans up the old city-keyed PropagationPolicies once nothing references them, while new policies use the location-keyed naming scheme. CLI, webhook, RBAC, CRDs, docs, and e2e fixtures were all updated consistently, and the new code is covered by solid table-driven tests. No missed call sites or migration gaps found.

feat(cli): publish workloads on a URL with --http-port
@scotwells
scotwells dismissed stale reviews from kevwilliams and ecv via 05d258f September 10, 2026 13:01
@scotwells

Copy link
Copy Markdown
Contributor Author

@ecv @kevwilliams had to resolve some merge conflicts with main. Diff is larger because I merged #289 into this branch by accident.

@scotwells
scotwells merged commit 8540f3b into main Sep 10, 2026
10 checks passed
@scotwells
scotwells deleted the feat/location-native-compute branch September 10, 2026 14:02
scotwells added a commit that referenced this pull request Sep 10, 2026
Main moved placement from city codes to locations (#264), added runtime
classes (#259), published a shared instance-type catalog, and grew
`datumctl compute deploy` a URL-publishing path (#289). The assistant's
workload-creation surface was rebuilt on top of those rather than beside
them.

Semantic resolutions:

- Placements are locations. `workloadspec.Placement` drops `CityCodes` for
  `Locations []string` plus `LocationSelector`, exactly one of which must be
  set; render emits `spec.placements[].locations` or the selector and refuses
  an empty or malformed one, the way admission does. The render tool's schema,
  its notes, the diff summary, the workload-create skill, llms-full.txt and
  the README all say "location" and point the model at
  `compute_locations_list` for the names, with `locationSelector` named as the
  way to place in every location of a city or region.

- Location resolution keeps main's structure. `SourceServiceAvailability` is
  gone; `locations.ListAvailableLocations` is built on main's
  `AvailableLocations` and errors with `ErrAvailabilityNotServed` when the
  control plane serves no availability, so `compute_locations_list` still
  fails loud rather than reporting a project with nowhere to run. The
  manager's placement reads, which treat an unserved kind as no gate at all,
  are unchanged. The tool now reports `placeable` and `ready` alongside the
  name, city code and topology.

- Instance types read `pkg/instancetype`. `validation.SupportedInstanceTypes`
  and the duplicated sizing table in the agent are deleted, along with
  TODO(#137): the catalog is the single source now.

- Runtime classes: `workloadspec.Input` gains an optional `RuntimeClass`,
  passed through verbatim with no default, so the server picks its own.

- `internal/cmd/compute/deploy/deploy.go` is main's version wholesale, for
  `--location`, `--location-selector`, `--city` and `--http-port`. The CLI no
  longer renders through workloadspec, so the parity test that pinned the two
  together is deleted rather than left stale.

- Docs take main's triage-skill changes with this branch's `compute_` renames
  on top, and workload-create now says that a public HTTPS URL is published
  separately — today only by `datumctl compute deploy --http-port` — so
  `ports` makes a port reachable on the instance's address, not on a managed
  URL.

Co-Authored-By: Claude Fable 5.1 <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.

4 participants