# networks (/docs/reference/api/networks)

> Networks are a tenant's private networks (VPCs) on a provider account, with their subnets and firewall rules ([firewallRules](/docs/reference/api/firewall-rules)). A network belongs to one provider pool, so to that pool's account, and the provider must support tenant networks (Google Cloud does; a provider that does not is `UNSUPPORTED`). The calls record what the tenant wants and return at once. The `vps.networks` reconciler then converges the provider: it creates the VPC, then the subnets and the firewall rules, then tags the network's instances, and removes what the tenant deleted, rules first, then subnets, then the network. `state` shows where it is: `pending`, `ready`, `deleting`, `deleted` or `error` (with a `message`). The network's `state` covers the VPC and its subnets (`ready` when all are; `error` with the failing resource in `message`); each subnet and each rule also has its own `state`, and a rule the provider refuses is `error` on the rule without taking the network out of `ready`. The reconciler publishes `network.ready`, `network.error`, `network.subnet.ready`, `network.rule.ready`, `network.rule.error` and `network.deleted`. An `error` is retried by the periodic pass, which runs every five minutes and also re-applies what drifted at the provider (a firewall deleted by hand).



Networks are a tenant's private networks (VPCs) on a provider account, with their subnets and firewall rules ([firewallRules](/docs/reference/api/firewall-rules)). A network belongs to one provider pool, so to that pool's account, and the provider must support tenant networks (Google Cloud does; a provider that does not is `UNSUPPORTED`). The calls record what the tenant wants and return at once. The `vps.networks` reconciler then converges the provider: it creates the VPC, then the subnets and the firewall rules, then tags the network's instances, and removes what the tenant deleted, rules first, then subnets, then the network. `state` shows where it is: `pending`, `ready`, `deleting`, `deleted` or `error` (with a `message`). The network's `state` covers the VPC and its subnets (`ready` when all are; `error` with the failing resource in `message`); each subnet and each rule also has its own `state`, and a rule the provider refuses is `error` on the rule without taking the network out of `ready`. The reconciler publishes `network.ready`, `network.error`, `network.subnet.ready`, `network.rule.ready`, `network.rule.error` and `network.deleted`. An `error` is retried by the periodic pass, which runs every five minutes and also re-applies what drifted at the provider (a firewall deleted by hand).

Only instances that own their machine are tagged: not deleting, lost or failed, and an attached one once it is claimed and while it has no `OwnershipLost`, `AttachConflict` or `DuplicateMachine` condition; the provider is told the instance id and leaves a machine labelled for another instance alone. Tags are set after the rules, and a machine whose tags were set within the last hour is skipped, so a network with more machines than one run can tag gets its rules at once and its tags over the next runs; a tag removed by hand comes back within the hour.

The provider-side name of a network, a subnet and a rule is `bc-` and the resource's own id, lower-cased. A machine on the network carries the tag `bc-` and 16 hex digits of the sha256 of its instance id, so a rule can target single instances.

Every tenant's networks on an account live in that account's one provider project, so they share its quotas (VPC networks, subnetworks and firewall rules per project): one tenant's networks use up what the others on the account can get. To bound that, a tenant has at most 10 networks that are not deleted, and a network at most 16 subnets and 100 firewall rules (`QUOTA_EXCEEDED`, with the cap in `details.limit`). The project's own quotas still apply on top, and a refusal of the provider shows as `error` with its message.

All 11 methods are served by @better-compute/server 0.3.0.

| Method                          | Kind     | Authorization                                      | Phase | Status     |
| ------------------------------- | -------- | -------------------------------------------------- | ----- | ---------- |
| [`create`](#create)             | mutation | `compute:networks:create` @ `compute-network`      | v1.0  | registered |
| [`list`](#list)                 | read     | `compute:networks:read` @ `compute-tenant` + items | v1.0  | registered |
| [`get`](#get)                   | read     | `compute:networks:read` @ `compute-network`        | v1.0  | registered |
| [`update`](#update)             | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |
| [`delete`](#delete)             | mutation | `compute:networks:delete` @ `compute-network`      | v1.0  | registered |
| [`addSubnet`](#addsubnet)       | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |
| [`removeSubnet`](#removesubnet) | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |
| [`adopt`](#adopt)               | mutation | `compute:providers:manage` @ `compute-fleet`       | v1.0  | registered |
| [`natGateway`](#natgateway)     | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |
| [`peer`](#peer)                 | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |
| [`routes`](#routes)             | mutation | `compute:networks:update` @ `compute-network`      | v1.0  | registered |

## create [#create]

`POST /api/compute/networks/create`

Records a network on the pool's provider account and returns it `pending`, with no subnets yet.

* The pool must be offered to the tenant, a `provider` pool, and its account's provider must support networks; otherwise the call is `UNSUPPORTED` (`NOT_FOUND` for a pool the tenant cannot see).
* `name` is 1 to 42 characters, `[a-z]([-a-z0-9]*[a-z0-9])?`, unique among the tenant's networks that are not deleted (`CONFLICT` otherwise; the name of a deleted network is free again).
* A tenant that already has 10 networks that are not deleted (one being deleted still counts) is `QUOTA_EXCEEDED`.
* Decided as `compute:networks:create` on the network it would create (`compute-network`, under the tenant), whose attributes are `providerAccountId`, `poolId` and `projectId`. A refusal leaves nothing behind.
* On a pool whose billing mode is `metered`, the tenant's budgets are checked after the decision, as for `instances.create`: when the pool asks for it or the tenant's offer is `stop-new`, `drain` or `suspend`, a spent budget is `SPEND_LIMIT_REACHED`. A root caller creating a network for a tenant is checked against that tenant's budgets. A suspended offer or a region its `allowedRegions` exclude refuses the create on any pool.

The network id derives from the tenant, the caller and `idempotencyKey`. A retry with the same key returns the same network; the same key with another pool, name, project or description is `CONFLICT`. The caller owns the network. Publishes `network.created`.

* **Kind:** mutation
* **Authorization:** `compute:networks:create` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type   | Required | Constraints                                                        |
| ---------------- | ------ | -------- | ------------------------------------------------------------------ |
| `tenantId`       | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `projectId`      | string |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `poolId`         | string | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `name`           | string | yes      | at most 42 characters; matches `^[a-z]([-a-z0-9]{0,40}[a-z0-9])?$` |
| `description`    | string |          | at most 256 characters                                             |
| `cidr`           | string |          |                                                                    |
| `idempotencyKey` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |

## list [#list]

`POST /api/compute/networks/list`

Returns the tenant's networks with their subnets, oldest first; `poolId` and `projectId` narrow it, and `includeDeleted: true` adds deleted ones. At most 500 are returned. Decided as `compute:networks:read` on the tenant.

* **Kind:** read
* **Authorization:** `compute:networks:read` @ `compute-tenant` + items
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type    | Required | Constraints                                                       |
| ---------------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId`       | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`         | string  |          | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `projectId`      | string  |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `includeDeleted` | boolean |          | default `false`                                                   |

## get [#get]

`POST /api/compute/networks/get`

Returns one network with its subnets (`pending`, `ready`, `deleting`, `deleted` or `error`). A network of another tenant is `NOT_FOUND` here, and is refused by the decision (`ACCESS_DENIED`) before that. Decided as `compute:networks:read` on the network.

* **Kind:** read
* **Authorization:** `compute:networks:read` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field       | Type   | Required | Constraints                                                      |
| ----------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`  | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `networkId` | string | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$` |

## update [#update]

`POST /api/compute/networks/update`

Changes the network's `description`. The name, pool and account never change, because the provider cannot rename a network. A network being deleted is `INVALID_STATE`. Decided as `compute:networks:update` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field         | Type   | Required | Constraints                                                      |
| ------------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`    | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `networkId`   | string | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `description` | string | yes      | at most 256 characters                                           |

## delete [#delete]

`POST /api/compute/networks/delete`

Marks the network, its subnets and its rules for deletion and returns it `deleting`, along with its routes, peerings (also those another network holds with it) and NAT gateways. The reconciler deletes the routes, peerings and gateways, then the rules, then the subnets, then the network, and then the network is `deleted` and publishes `network.deleted`.

* `INVALID_STATE` while instances that are not deleted are on the network, with their ids in `details.instanceIds`: delete or move them first. Likewise while load balancers are on one of its subnets (`details.loadBalancerIds`).
* `INVALID_STATE` while [filesystems](/docs/reference/api/filesystems) that are not deleted are on the network, with their ids in `details.filesystemIds`: delete them first.
* `INVALID_STATE` while shared provider volumes that are not deleted are reached over the network, with their ids in `details.volumeIds`: delete them first.
* Deleting a network that is being deleted returns it unchanged.

Publishes `network.deleting`. Decided as `compute:networks:delete` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:delete` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field       | Type   | Required | Constraints                                                      |
| ----------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`  | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `networkId` | string | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$` |

## addSubnet [#addsubnet]

`POST /api/compute/networks/addSubnet`

Adds a subnet and returns the network with it `pending`.

* `cidr` is an IPv4 range inside 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16 (RFC 1918), with a prefix from /8 to /29, written as its network address (`10.0.0.0/20`). Anything else is `INVALID_INPUT`.
* It must not overlap another subnet of the network, in any region: that is `CONFLICT`, and the database refuses it too.
* `region` must be a region of the zones the account is configured with (`us-central1` for the zone `us-central1-a`); otherwise `INVALID_INPUT`, with the allowed regions in `details`.
* The name is unique in the network; the name of a deleted subnet is free again.
* A network that already has 16 subnets (not counting removed ones) is `QUOTA_EXCEEDED`.
* A network being deleted is `INVALID_STATE`.

Publishes `network.subnet.ready` once the provider has the subnet. Decided as `compute:networks:update` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field           | Type    | Required | Constraints                                                        |
| --------------- | ------- | -------- | ------------------------------------------------------------------ |
| `tenantId`      | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `networkId`     | string  | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `name`          | string  | yes      | at most 42 characters; matches `^[a-z]([-a-z0-9]{0,40}[a-z0-9])?$` |
| `region`        | string  | yes      | matches `^[a-z][a-z0-9-]{1,38}[a-z0-9]$`                           |
| `zone`          | string  |          | matches `^[a-z][a-z0-9-]{1,62}[a-z0-9]$`                           |
| `cidr`          | string  | yes      |                                                                    |
| `privateAccess` | boolean |          |                                                                    |

## removeSubnet [#removesubnet]

`POST /api/compute/networks/removeSubnet`

Marks a subnet for deletion and returns the network with the subnet `deleting`. `INVALID_STATE` while instances that are not deleted use the subnet, with their ids in `details.instanceIds`, or load balancers are on it (`details.loadBalancerIds`), and while shared provider volumes of the network that are not deleted are in the subnet's region (a share lives in one of the network's VPCs there), with their ids in `details.volumeIds`. Its range is free for a new subnet at once. Decided as `compute:networks:update` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field       | Type   | Required | Constraints                                                       |
| ----------- | ------ | -------- | ----------------------------------------------------------------- |
| `tenantId`  | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `networkId` | string | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `subnetId`  | string | yes      | at most 57 characters; matches `^snet_[0-9a-hjkmnp-tv-z]{26,52}$` |

## adopt [#adopt]

`POST /api/compute/networks/adopt`

Registers a VPC that already exists in the pool's provider account as a network of the tenant, so attached VMs whose NIC is on it are linked to it and get its firewall rules; returns the network `pending`, with `externalNetwork` set. Only the platform (root callers) adopts.

* **Input.** `externalNetwork` is the VPC's provider-side name, 1 to 63 characters `[a-z]([-a-z0-9]*[a-z0-9])?`; a `bc-` name (a network better-compute made) is `INVALID_INPUT`. `default` is shared with every VM of the project: adopting it needs `sharedTagScoped: true` (`INVALID_INPUT` with `details.field: 'sharedTagScoped'` otherwise), which the network records and shows. `name` and `description` are as for `create`, and the 10-network cap applies.

* **The VPC.** better-compute never creates, changes or deletes an adopted VPC: the reconciler skips it, provisions only the subnets and rules added to the network, and `delete` removes those and leaves the VPC. Its provider-side name is `externalNetwork`, not `bc-` and the id, also for instances created on it.

* **Firewall rules.** A rule on an adopted network must name `targets.instanceIds` (`INVALID_INPUT` on `firewallRules.create` and `update` otherwise), so it always targets instance network tags and never the whole VPC. A rule without targets that reaches the reconciler anyway goes to `error` instead of being applied.

* **Attach.** `instances.attach` links a machine whose NIC is on the VPC (by its name, or a reference ending in it) to the network, as it does for networks better-compute made.

* **Idempotency.** The same adoption again (same pool, VPC, name, flag and description) returns the network. A VPC already adopted in the tenant under another request is `CONFLICT` with `details.networkId`; one adopted by another tenant is `CONFLICT` unless both adoptions are `sharedTagScoped`.

* **IAM.** Decided as `compute:providers:manage` on the fleet (`compute-fleet`, in the root tenant); anyone but the platform is `ACCESS_DENIED`. The adopting identity owns the network.

* **Events.** `network.adopted` with `poolId`, `providerAccountId`, `name`, `externalNetwork` and `sharedTagScoped`.

* **Kind:** mutation

* **Authorization:** `compute:providers:manage` @ `compute-fleet`

* **Module:** `vps`

* **Phase:** v1.0 (registered)

**Input**

| Field             | Type    | Required | Constraints                                                        |
| ----------------- | ------- | -------- | ------------------------------------------------------------------ |
| `tenantId`        | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `poolId`          | string  | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `externalNetwork` | string  | yes      | at most 63 characters; matches `^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$` |
| `name`            | string  | yes      | at most 42 characters; matches `^[a-z]([-a-z0-9]{0,40}[a-z0-9])?$` |
| `sharedTagScoped` | boolean |          | default `false`                                                    |
| `description`     | string  |          | at most 256 characters                                             |

## natGateway [#natgateway]

`POST /api/compute/networks/natGateway`

Turns a subnet's NAT gateway on (`enabled: true`) or off (`enabled: false`) and returns the network; the gateway appears in `natGateways` (`pending`, then `ready` with its public egress `addresses`). A NAT gateway carries the subnet's outbound traffic, so its machines need no public address of their own (isolated pools, and DOKS isolated worker nodes). Only providers with NAT gateways take it (DigitalOcean does; Google Cloud is `UNSUPPORTED` here).

* One gateway per subnet: asking again returns the network unchanged, and turning off a subnet without one is a no-op.
* Turning it off is `INVALID_STATE`, with the route names in `details.routes`, while a route of the subnet sends traffic to it (`networks.routes`): change those routes first.
* The subnet must belong to the network and not be being removed (`NOT_FOUND`, `INVALID_STATE`); a network being deleted is `INVALID_STATE`.
* Removing the subnet or deleting the network removes the gateway first: the reconciler deletes the subnet only once the gateway is gone.

Publishes `network.natGateway.ready` (and `network.natGateway.error`) with `edgeId` and `subnetId`. Decided as `compute:networks:update` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field       | Type    | Required | Constraints                                                       |
| ----------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId`  | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `networkId` | string  | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `subnetId`  | string  | yes      | at most 57 characters; matches `^snet_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `enabled`   | boolean | yes      |                                                                   |

## peer [#peer]

`POST /api/compute/networks/peer`

Peers a subnet of the network (`subnetId`) with a subnet of another network of the tenant (`peerNetworkId`, `peerSubnetId`), so their machines reach each other on private addresses; `enabled: false` removes the peering. Two subnets of the same network may be peered too: on DigitalOcean every subnet is a VPC of its own, and two of them do not reach each other otherwise. Only providers with peerings take it (`UNSUPPORTED` otherwise).

* The peering appears in `peerings` on both networks, each seen from its own side (`subnetId` is that network's subnet). The same pair asked again, from either network, is the same peering; either network can remove it.
* Both networks must be the tenant's (another tenant's network is `NOT_FOUND`), on the same provider account (`INVALID_INPUT`), and not being deleted (`INVALID_STATE`). A subnet is never peered with itself (`INVALID_INPUT`).
* A network takes part in at most 16 peerings (`QUOTA_EXCEEDED`, with `details.limit` and the network in `details.networkId`).
* Removing either subnet, or deleting either network, removes the peering first; the subnet goes once the peering is gone.

Publishes `network.peering.ready` (and `network.peering.error`) on the network that asked first. Decided as `compute:networks:update` on the network, and, on the caller's own credential, on the other network too.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field           | Type    | Required | Constraints                                                       |
| --------------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId`      | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `networkId`     | string  | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `subnetId`      | string  | yes      | at most 57 characters; matches `^snet_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `peerNetworkId` | string  | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `peerSubnetId`  | string  | yes      | at most 57 characters; matches `^snet_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `enabled`       | boolean |          | default `true`                                                    |

## routes [#routes]

`POST /api/compute/networks/routes`

Replaces the static routes of a subnet with `routes` (at most 32) and returns the network; the routes appear in `routes`. A route kept exactly as it was stays; a changed one is replaced (deleted at the provider, then made anew); one left out is deleted; an empty list removes them all. Each route has a `name` (unique in the subnet), a `destination` IPv4 CIDR written as its network address (`0.0.0.0/0` for everything), and exactly one `nextHop`:

* `{ natGateway: true }`: the subnet's NAT gateway, which must exist (`INVALID_STATE` otherwise). The reconciler makes the route once the gateway is `ready`.
* `{ instanceId }`: an instance of the tenant on this subnet (`NOT_FOUND`, or `INVALID_INPUT` when it is on another subnet). The route waits until the instance has a machine; an instance that is gone later puts the route in `error`. Decided, besides the network, as `compute:instances:update` on the instance, on the caller's own credential.
* `{ address }`: a host address inside the subnet (`INVALID_INPUT` otherwise).

Routes are a preview at DigitalOcean (VPC Routes): the account needs `previews: true`, or the route goes to `error` with the provider's refusal. Only providers with static routes take a non-empty list (`UNSUPPORTED` otherwise).

Publishes `network.route.ready` (and `network.route.error`). Decided as `compute:networks:update` on the network.

* **Kind:** mutation
* **Authorization:** `compute:networks:update` @ `compute-network`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field                         | Type           | Required | Constraints                                                        |
| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------ |
| `tenantId`                    | string         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`         |
| `networkId`                   | string         | yes      | at most 56 characters; matches `^net_[0-9a-hjkmnp-tv-z]{26,52}$`   |
| `subnetId`                    | string         | yes      | at most 57 characters; matches `^snet_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `routes`                      | list of object | yes      | 0-32 items                                                         |
| `routes[].name`               | string         | yes      | at most 42 characters; matches `^[a-z]([-a-z0-9]{0,40}[a-z0-9])?$` |
| `routes[].destination`        | string         | yes      |                                                                    |
| `routes[].nextHop`            | object         | yes      |                                                                    |
| `routes[].nextHop.natGateway` | any            |          |                                                                    |
| `routes[].nextHop.instanceId` | string         |          | at most 57 characters; matches `^inst_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `routes[].nextHop.address`    | string         |          |                                                                    |
