Skip to content

Conversation

@zeeshanlakhani
Copy link
Collaborator

@zeeshanlakhani zeeshanlakhani commented Jan 6, 2026

This PR refactors FloatingIpCreate, EphemeralIpCreate, ExternalIpCreate::Ephemeral, and ProbeCreate to use tagged enums for pool selection, making invalid states unrepresentable at the type level.

Previously, these types used flat Option fields (pool, ip_version, ip) that required runtime validation to catch invalid combinations like ip_version + pool or ip + ip_version. The new design uses:

  • PoolSelection enum with Named { pool } and Default { ip_version } variants, used by EphemeralIpCreate, ExternalIpCreate::Ephemeral, and ProbeCreate
  • FloatingIpAllocation enum with Explicit { ip, pool } and Auto { pool_selection } variants for FloatingIpCreate

This ensures at the type level that...

  • ip_version can only be specified when using the default pool
  • ip_version cannot be combined with an explicit IP address
  • Named pools don't accept ip_version (the pool determines available IPs)

The Explicit { ip, pool } combination remains valid for reserving an exact IP, validating it exists in this pool (pool is a constraint, not a selection method).

API versioning: Added v2026010300.rs with the old flat types and TryFrom implementations that validate and convert to the new enum-based types for delegation. Older API versions continue to work via the existing conversion chain.

ProbeCreate in the experimental API previously lacked ip_version support; this is now addressed using the enum approach.

… enums (type-safe API)

This PR refactors `FloatingIpCreate`, `EphemeralIpCreate`, `ExternalIpCreate::Ephemeral`,
and `ProbeCreate` to use tagged enums for pool selection, making invalid states
unrepresentable at the type level.

Previously, these types used flat `Option` fields (`pool`, `ip_version`, `ip`)
that required runtime validation to catch invalid combinations like
`ip_version + pool` or `ip + ip_version`. The new design uses:

  - `PoolSelection` enum with `Named { pool }` and `Default { ip_version }`
    variants, used by `EphemeralIpCreate`, `ExternalIpCreate::Ephemeral`,
    and `ProbeCreate`

  - `FloatingIpAllocation` enum with `Explicit { ip, pool }` and
    `Auto { pool_selection }` variants for `FloatingIpCreate`

  This ensures at the type level that...
  - `ip_version` can only be specified when using the default pool
  - `ip_version` cannot be combined with an explicit IP address
  - Named pools don't accept `ip_version` (the pool determines available IPs)

The `Explicit { ip, pool }` combination remains valid for reserving
an exact IP, validating it exists in this pool (pool is a constraint, not
a selection method).

API versioning: Added v2026010300.rs with the old flat types and `TryFrom`
implementations that validate and convert to the new enum-based types for delegation.
Older API versions continue to work via the existing conversion chain.

Note: ProbeCreate in the experimental API previously lacked ip_version support; this is now addressed using the enum approach.
@zeeshanlakhani zeeshanlakhani marked this pull request as ready for review January 6, 2026 07:49
@zeeshanlakhani zeeshanlakhani requested review from ahl and bnaecker January 6, 2026 07:49
@david-crespo
Copy link
Contributor

I like the general idea. I think I prefer something more like "selector" over "allocation" for both levels. It looks like you're trying to avoid calling them both the same thing, but maybe it's ok if one is poolSelector and the other is ipSelector or addressSelector? My worry with selector is that we'd be overloading the term slightly, because we use it broadly in the API to refer to those { instance: string, project: string } things we extract from path and query params to identify resource. This is kind of the same thing, though you could argue that when you select an IP by saying what pool you want it from, you're not really selecting an IP.

Generated diff for TS client
diff --git a/tmp/api-diff/2e00484cf882af3c450d1cb3124677c6a4d0d374/nexus-2026010300.0.0-7599dd.json/Api.ts b/tmp/api-diff/2e00484cf882af3c450d1cb3124677c6a4d0d374/nexus-2026010500.0.0-423571.json/Api.ts
index 989e2d63..adab6be3 100644
--- a/tmp/api-diff/2e00484cf882af3c450d1cb3124677c6a4d0d374/nexus-2026010300.0.0-7599dd.json/Api.ts
+++ b/tmp/api-diff/2e00484cf882af3c450d1cb3124677c6a4d0d374/nexus-2026010500.0.0-423571.json/Api.ts
@@ -1931,14 +1931,31 @@ export type Distributionint64 = {
  */
 export type IpVersion = "v4" | "v6";
 
+/**
+ * Pool selection for IP address allocation.
+ *
+ * This enum makes invalid states unrepresentable: `ip_version` is only relevant when using the default pool.
+ */
+export type PoolSelection =
+  /** Use the specified pool by name or ID. */
+  | {
+      /** The pool to allocate from. */
+      pool: NameOrId;
+      type: "named";
+    }
+  /** Use the default pool for the silo. */
+  | {
+      /** IP version to use when multiple default pools exist. Required if both IPv4 and IPv6 default pools are configured. */
+      ipVersion?: IpVersion | null;
+      type: "default";
+    };
+
 /**
  * Parameters for creating an ephemeral IP address for an instance.
  */
 export type EphemeralIpCreate = {
-  /** IP version to use when allocating from the default pool. Only used when `pool` is not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available. */
-  ipVersion?: IpVersion | null;
-  /** Name or ID of the IP pool used to allocate an address. If unspecified, the default IP pool will be used. */
-  pool?: NameOrId | null;
+  /** Pool to allocate from. */
+  poolSelection?: PoolSelection;
 };
 
 export type ExternalIp =
@@ -1984,11 +2001,10 @@ SNAT addresses are ephemeral addresses used only for outbound connectivity. */
  * Parameters for creating an external IP address for instances.
  */
 export type ExternalIpCreate =
-  /** An IP address providing both inbound and outbound access. The address is automatically assigned from the provided IP pool or the default IP pool if not specified. */
+  /** An IP address providing both inbound and outbound access. The address is automatically assigned from a pool. */
   | {
-      /** IP version to use when allocating from the default pool. Only used when `pool` is not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available. */
-      ipVersion?: IpVersion | null;
-      pool?: NameOrId | null;
+      /** Pool to allocate from. */
+      poolSelection?: PoolSelection;
       type: "ephemeral";
     }
   /** An IP address providing both inbound and outbound access. The address is an existing floating IP object assigned to the current project.
@@ -2115,6 +2131,27 @@ export type FloatingIp = {
   timeModified: Date;
 };
 
+/**
+ * How to allocate a floating IP address.
+ *
+ * This enum makes invalid states unrepresentable: when specifying an explicit IP, there's no `ip_version` field (the IP determines its version). The `ip_version` preference is only available when auto-allocating.
+ */
+export type FloatingIpAllocation =
+  /** Reserve a specific IP address. */
+  | {
+      /** The specific IP address to reserve. Must be available in the pool. */
+      ip: string;
+      /** The pool containing this address. If not specified, the default pool for this IP's version is used. */
+      pool?: NameOrId | null;
+      type: "explicit";
+    }
+  /** Automatically allocate an IP address based on pool selection. */
+  | {
+      /** How to select the pool for allocation. */
+      poolSelection?: PoolSelection;
+      type: "auto";
+    };
+
 /**
  * The type of resource that a floating IP is attached to
  */
@@ -2134,14 +2171,10 @@ export type FloatingIpAttach = {
  * Parameters for creating a new floating IP address for instances.
  */
 export type FloatingIpCreate = {
+  /** How to allocate the floating IP address. */
+  allocation?: FloatingIpAllocation;
   description: string;
-  /** An IP address to reserve for use as a floating IP. This field is optional: when not set, an address will be automatically chosen from `pool`. If set, then the IP must be available in the resolved `pool`. */
-  ip?: string | null;
-  /** IP version to use when allocating from the default pool. Only used when both `ip` and `pool` are not specified. Required if multiple default pools of different IP versions exist. Allocation fails if no pool of the requested version is available. */
-  ipVersion?: IpVersion | null;
   name: Name;
-  /** The parent IP pool that a floating IP is pulled from. If unset, the default pool is selected. */
-  pool?: NameOrId | null;
 };
 
 /**
@@ -3537,8 +3570,9 @@ export type Probe = {
  */
 export type ProbeCreate = {
   description: string;
-  ipPool?: NameOrId | null;
   name: Name;
+  /** Pool selection for allocating an ephemeral IP to the probe. */
+  poolSelection?: PoolSelection;
   sled: string;
 };
 
@@ -7048,7 +7082,7 @@ export class Api {
    * Pulled from info.version in the OpenAPI schema. Sent in the
    * `api-version` header on all requests.
    */
-  apiVersion = "2026010300.0.0";
+  apiVersion = "2026010500.0.0";
 
   constructor({ host = "", baseParams = {}, token }: ApiConfig = {}) {
     this.host = host;

…dressSelector

Renames the tagged enums for IP pool and address allocation to use consistent "Selector" suffix:

  - `PoolSelection` -> `PoolSelector`
  - `FloatingIpAllocation` → `AddressSelector`
  - `pool_selection` → `pool_selector` (field in AddressSelector::Auto, ExternalIpCreate::Ephemeral, EphemeralIpCreate, ProbeCreate)
  - `allocation` → `address_selector` (field in FloatingIpCreate)

This addresses review feedback requesting consistent naming between the two related enums. While "Selector" is used elsewhere in the API for
path/query parameter structs (e.g., ProjectSelector, FloatingIpSelector), these enums serve a similar "selection" purpose in request bodies, and
the consistent naming improves API ergonomics.

Note: `selector` is a bit overused, but choosing consistency seems better here.
Copy link
Contributor

@ahl ahl left a comment

Choose a reason for hiding this comment

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

I like it! what do you think?

Pulled this change into the CLI to check it out:

@@ -278,8 +278,9 @@ impl crate::AuthenticatedCmd for CmdInstanceFromImage {
                         size: self.size.clone(),
                     })
                     .external_ips(vec![ExternalIpCreate::Ephemeral {
-                        ip_version: Some(IpVersion::V4),
-                        pool: None,
+                        pool_selector: PoolSelector::Default {
+                            ip_version: Some(IpVersion::V4),
+                        },
                     }])
                     .hostname(self.hostname.clone())
                     .memory(self.memory.clone())

Seems nice?

@zeeshanlakhani
Copy link
Collaborator Author

@ahl, @david-crespo good to merge?

@ahl
Copy link
Contributor

ahl commented Jan 7, 2026

@ahl, @david-crespo good to merge?

agreed; fire when ready

@zeeshanlakhani zeeshanlakhani enabled auto-merge (squash) January 7, 2026 18:53
@zeeshanlakhani zeeshanlakhani enabled auto-merge (squash) January 7, 2026 19:00
@zeeshanlakhani zeeshanlakhani self-assigned this Jan 8, 2026
@zeeshanlakhani zeeshanlakhani merged commit b3fa1b5 into main Jan 8, 2026
16 checks passed
@zeeshanlakhani zeeshanlakhani deleted the zl/ip-version-follow-up branch January 8, 2026 01:16
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