Skip to content

enhancement: validate uuid attributes#795

Merged
sudomateo merged 3 commits into
mainfrom
sudomateo/vroqvovxkyro
Jul 7, 2026
Merged

enhancement: validate uuid attributes#795
sudomateo merged 3 commits into
mainfrom
sudomateo/vroqvovxkyro

Conversation

@sudomateo

Copy link
Copy Markdown
Collaborator

Attributes that expect UUIDs now validate whether their values are, in fact, valid UUIDs. This catches cases where names were passed to attributes accidentally worked.

Closes SSE-347.

@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch 2 times, most recently from 8dd8460 to ccb9000 Compare July 1, 2026 01:51
@sudomateo
sudomateo marked this pull request as ready for review July 1, 2026 01:53
@sudomateo
sudomateo requested a review from a team as a code owner July 1, 2026 01:53

@lgfa29 lgfa29 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was wondering if it would make sense to implement a custom type, but I don't think it would give us much more than this validation.

@lgfa29

lgfa29 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Hum....something that I just thought about is that the migration path here is not straightforward. Since most *_id attributes have a RequiresReplace(), if you modify it from a name to an UUID Terraform will try to destroy it 😳

We may need a plan modifier to ignore changes from non-UUID to UUID values.

Maybe a custom type may make sense after all? We could have a non-UUID and an UUID considered as "semantically equal" to avoid this problem?

@sudomateo

Copy link
Copy Markdown
Collaborator Author

From what I researched, RequiresReplace does not call the custom type's StringSemanticEquals. Instead, it'll call ToStringValue to make the custom type some string representation and then do a raw comparison between previous and current values.

Even so, almost all the uses of *_id attributes take a name or ID on Create but then, during Read, update the value to the UUID returned from the Oxide API. That means the value in state is already the UUID.

Only the following resources don't have this behavior, each for their listed reason.

  • oxide_image.source_snapshot_id: The source snapshot ID is not returned by the API, but the create API only accepts a UUID so users could never pass a name.
  • oxide_vpc_firewall_rules.vpc_id: The VPC ID is not returned by the API. When we have at least one firewall rule, we use the VPC ID returned from the API on the first rule. The value in state is already a UUID.
  • oxide_ip_pool_silo_link.silo_id: Name or ID works today. This resource is why I submitted this patch since a customer got bit by this.
  • oxide_subnet_pool_silo_link.silo_id: Basically the same thing as the above link resource but no one got bit by this yet.
  • oxide_subnet_pool_member.subnet_pool_id: Name or ID works here. That's actually correct behavior so technically we should rename the attribute subnet_pool instead of subnet_pool_id. Probably something we come back to when we decide on our NameOrId behavior. I'll remove the IsUUID validator from this one.

Here's the table my trusty LLM agent helped me confirm.

Resource / Data source Attribute Refreshed to UUID on Read? API accepts NameOrId? Replace risk Justification
anti_affinity_group project_id Yes Yes None Read writes antiAffinityGroup.ProjectId, so a name self-heals to the UUID on refresh before the plan compare.
disk project_id Yes Yes None Read writes disk.ProjectId.
disk source_image_id Yes (when set) No None UUID-only API field (DiskSourceImage.ImageId); Read writes disk.ImageId when present.
disk source_snapshot_id Yes (when set) No None UUID-only API field (DiskSourceSnapshot.SnapshotId); Read writes disk.SnapshotId when present.
external_subnet project_id Yes Yes None Read writes externalSubnet.ProjectId.
external_subnet subnet_pool_id Yes Yes None Read writes externalSubnet.SubnetPoolId.
external_subnet_attachment external_subnet_id Yes Yes None Read writes externalSubnet.Id.
external_subnet_attachment instance_id Yes Yes None Read writes externalSubnet.InstanceId.
floating_ip ip_pool_id Yes Yes None Read writes floatingIP.IpPoolId.
floating_ip project_id Yes Yes None Read writes floatingIP.ProjectId.
image project_id Yes (when set) Yes None Read writes image.ProjectId when non-empty.
image source_snapshot_id No No None API field is UUID-only, never a name, so lack of refresh cannot cause a name→UUID replace.
instance project_id Yes Yes None Read writes instance.ProjectId.
instance boot_disk_id Yes Yes None Read writes instance.BootDiskId.
instance network_interfaces[].subnet_id Yes Yes None Read writes nic.SubnetId.
instance network_interfaces[].vpc_id Yes Yes None Read writes nic.VpcId.
instance external_ips[].ip_pool_id Yes Yes None Read writes v.IpPoolId; attribute is Computed/optional, not RequiresReplace.
snapshot project_id Yes Yes None Read writes snapshot.ProjectId.
snapshot disk_id Yes Yes None Read writes snapshot.DiskId.
switch_port_settings ...addresses[].address_lot_id Yes Yes None Read writes address.AddressLotId via toSwitchPortSettingsModel.
vpc project_id Yes Yes None Read writes vpc.ProjectId.
vpc_firewall_rules vpc_id Conditional Yes Low Read writes firewallRules.Rules[0].VpcId only when len(Rules) > 0; a VPC with zero rules keeps a configured name in state.
vpc_internet_gateway vpc_id Yes Yes None Read writes vpcInternetGateway.VpcId.
vpc_router vpc_id Yes Yes None Read writes vpcRouter.VpcId.
vpc_router_route vpc_router_id Yes Yes None Read writes vpcRouterRoute.VpcRouterId.
vpc_subnet vpc_id Yes Yes None Read writes subnet.VpcId.
ip_pool_silo_link ip_pool_id Yes Yes None Read writes pools[idx].Id (matches on both ID and name).
ip_pool_silo_link silo_id No Yes High API never returns the silo ID, so Read can't canonicalize; a configured name persists and a correction to UUID triggers replace. Validator is the intended guardrail.
subnet_pool_silo_link subnet_pool_id Yes Yes None Read writes pools[idx].Id.
subnet_pool_silo_link silo_id No Yes High API never returns the silo ID; same exposure as ip_pool_silo_link.silo_id.
subnet_pool_member subnet_pool_id No Yes Medium Read writes id/subnet/prefixes/time but not the pool ID; a configured name persists and correction to UUID triggers replace. Low real-world usage.
images (data source) project_id N/A Yes None Query input, not stored state; no RequiresReplace semantics (validator only restricts name lookups).
instance_external_ips (data source) instance_id N/A Yes None Query input, not stored state; no RequiresReplace semantics (validator only restricts name lookups).

@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch from ccb9000 to d4fa921 Compare July 3, 2026 01:48
@lgfa29

lgfa29 commented Jul 3, 2026

Copy link
Copy Markdown
Member

From what I researched, RequiresReplace does not call the custom type's StringSemanticEquals. Instead, it'll call ToStringValue to make the custom type some string representation and then do a raw comparison between previous and current values.

Ah ok. From the docs it sounded like it would do the thing we expect, but I guess it doesn't 😅

Even so, almost all the uses of *_id attributes take a name or ID on Create but then, during Read, update the value to the UUID returned from the Oxide API. That means the value in state is already the UUID.

Hum....but that's also not a good thing because I think the apply will fail with a produced an unexpected new value since the config and state won't match. I guess that's better than silently accepting a name when we expect an UUID, and it probably means that users already had to fix their config to use UUIDs before we add this validation, so we probably don't need to worry about the migration path for them.

@sudomateo

Copy link
Copy Markdown
Collaborator Author

Hum....but that's also not a good thing because I think the apply will fail with a produced an unexpected new value since the config and state won't match. I guess that's better than silently accepting a name when we expect an UUID, and it probably means that users already had to fix their config to use UUIDs before we add this validation, so we probably don't need to worry about the migration path for them.

Exactly, we don't need to worry about the migration path because anyone who was mistakenly using the name instead of the ID would have been shown a replacement in their plan like so. We can safely accept that, outside, the resources and attributes I already flagged, users already are using the UUID.

> cat main.tf
...
resource "oxide_anti_affinity_group" "test" {
  name        = "test"
  description = "test"
  policy      = "allow"
  project_id  = "matthewsanabria"
}


> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # oxide_anti_affinity_group.test will be created
  + resource "oxide_anti_affinity_group" "test" {
      + description    = "test"
      + failure_domain = (known after apply)
      + id             = (known after apply)
      + name           = "test"
      + policy         = "allow"
      + project_id     = "matthewsanabria"
      + time_created   = (known after apply)
      + time_modified  = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

oxide_anti_affinity_group.test: Creating...
oxide_anti_affinity_group.test: Creation complete after 1s [id=42c8e44d-0c50-4811-b1fa-93d68e4780ca]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


> terraform apply
oxide_anti_affinity_group.test: Refreshing state... [id=42c8e44d-0c50-4811-b1fa-93d68e4780ca]

Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # oxide_anti_affinity_group.test must be replaced
-/+ resource "oxide_anti_affinity_group" "test" {
      ~ failure_domain = "sled" -> (known after apply)
      ~ id             = "42c8e44d-0c50-4811-b1fa-93d68e4780ca" -> (known after apply)
        name           = "test"
      ~ project_id     = "5476ccc9-464d-4dc4-bfc0-5154de1c986f" -> "matthewsanabria" # forces replacement
      ~ time_created   = "2026-07-04 00:23:57.656134 +0000 UTC" -> (known after apply)
      ~ time_modified  = "2026-07-04 00:23:57.656134 +0000 UTC" -> (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: no

Apply cancelled.


> terraform show
# oxide_anti_affinity_group.test:
resource "oxide_anti_affinity_group" "test" {
    description    = "test"
    failure_domain = "sled"
    id             = "42c8e44d-0c50-4811-b1fa-93d68e4780ca"
    name           = "test"
    policy         = "allow"
    project_id     = "matthewsanabria"
    time_created   = "2026-07-04 00:23:57.656134 +0000 UTC"
    time_modified  = "2026-07-04 00:23:57.656134 +0000 UTC"
}

@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch 2 times, most recently from 36ff80c to 69d6668 Compare July 4, 2026 00:33
@lgfa29

lgfa29 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Makes sense 👍

But we still need a plan modifier to avoid this right?

~ project_id     = "5476ccc9-464d-4dc4-bfc0-5154de1c986f" -> "matthewsanabria" # forces replacement

Copy link
Copy Markdown
Collaborator Author

I should have clarified that the example code shows how things work today without the changes in this patch. I wanted to show that users who use the name instead of the ID see that replacement and likely would have already filed some issue to get it fixed if they ran into it in production. This makes me believe that we're okay to implement the validation in this patch.

When we have validation this behavior won't be allowed and the user will be forced to use a UUID which would match the state that we've already been writing.

We don't need the plan modifier for these resources since we're already writing their UUID into state today. What makes you believe we'd need one?

@lgfa29

lgfa29 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Oh right sorry, I got confused with the order the plan output because I was thinking about the other way.

The scenario I'm thinking about is for the cases where we end up writing the name in state instead of ID, like for oxide_ip_pool_silo_link.silo_id.

In this case, I think the order of events would go something like:

  1. User terraform apply a config with silo_id = "my-silo" instead of an UUID using v0.20.1 of the provider.
  2. Apply succeeds and silo_id = "my-silo" in state. User doesn't notice any problem.
  3. User updates to v0.21.0, which has this UUID validation.
  4. terraform apply now fails because silo_id = "my-silo" is no longer valid.
  5. User updates silo_id to use the silo UUID.
  6. terraform apply shows a destructive replacement because silo_id has stringplanmodifier.RequiresReplace().

The user would have to do statefile surgery to get around this, but that's not ideal. The plan modifier I mentioned is for making the non-UUID to UUID change of silo_id (and potentially other attributes) non-destructive.

Copy link
Copy Markdown
Collaborator Author

Yeah you're right. I'm not sure what the plan modifier would look like in this case though. The requires replace logic will check for raw string equality so it'll want to replace the resource either way I think. I'll hack at it a bit.

@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch from 69d6668 to 17f47db Compare July 6, 2026 18:29

Copy link
Copy Markdown
Collaborator Author

Okay I think I got it with the latest push.

@lgfa29

lgfa29 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Yup, it seems to be working now and it doesn't try to replace the resource.

Terraform will perform the following actions:

  # oxide_ip_pool_silo_link.link will be updated in-place
  ~ resource "oxide_ip_pool_silo_link" "link" {
        id         = "185dbaf2-60b1-4665-b8b5-d089870acfd4/test-suite-silo"
      ~ silo_id    = "test-suite-silo" -> "c7d7c06c-2481-4fae-a42f-59048ac54b5c"
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

I think there are two new problems now:

  1. if you use an incorrect UUID (meaning, an UUID that doesn't correspond to the previous name) then the wrong ID is written to state. We may need to check that link.SiloId matches plan.SiloID on update and error if they don't.
  2. updating the resource ID on Read as was done in oxide_ip_pool_silo_link: use composite attributes for id #794 seems to be problematic because it uses state.SiloID, which can be the name in incorrect setups. It may be worth adding a temporary SiloView request to make sure the silo_id we write to ID is an UUID, not a name.
oxide_ip_pool_silo_link.link: Modifying... [id=185dbaf2-60b1-4665-b8b5-d089870acfd4/test-suite-silo]
╷
│ Error: Provider produced inconsistent result after apply
│
│ When applying changes to oxide_ip_pool_silo_link.link, provider "provider[\"registry.terraform.io/oxidecomputer/oxide\"]" produced an unexpected new value: .id: was
│ cty.StringVal("185dbaf2-60b1-4665-b8b5-d089870acfd4/test-suite-silo"), but now cty.StringVal("185dbaf2-60b1-4665-b8b5-d089870acfd4/59899ace-1d01-48a1-8a5e-9cabe28dcb7a").
│
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch from 17f47db to 170d98e Compare July 6, 2026 21:10

sudomateo commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

After I solved 2 then I get this behavior on 1.

I had 49438a11-9ded-46e2-b2be-7ae9a9fd0109/showcase in my state. Then I updated the configuration to a different silo's UUID a705588a-3e62-4c19-a227-c073476c33fc.

I see the refresh correctly upgraded my id to 49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658 which is the showcase UUID. This makes sense because Read refreshes from state no config. Then the plan shows that it wants to replace the resource which is correct because that's what the user configured.

  # oxide_ip_pool_silo_link.example must be replaced
-/+ resource "oxide_ip_pool_silo_link" "example" {
      ~ id         = "c6f8c195-8cee-40b5-bc95-7eb86edec285/338bfaf2-75d3-4a1c-8850-32173960f658" -> (known after apply)
      ~ silo_id    = "338bfaf2-75d3-4a1c-8850-32173960f658" -> "a705588a-3e62-4c19-a227-c073476c33fc" # forces replacement
        # (2 unchanged attributes hidden)
    }

  # oxide_subnet_pool_silo_link.example must be replaced
-/+ resource "oxide_subnet_pool_silo_link" "example" {
      ~ id             = "49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658" -> (known after apply)
      ~ silo_id        = "338bfaf2-75d3-4a1c-8850-32173960f658" -> "a705588a-3e62-4c19-a227-c073476c33fc" # forces replacement
        # (2 unchanged attributes hidden)
    }

@lgfa29

lgfa29 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Yeah, 2 seems to be fixed now, but the problem with 1 is when you set the UUID wrong from the beginning. To provide more details on the test I'm running:

  1. Using v0.20.1, create an oxide_ip_pool_silo_link with silo_id set to name instead of an UUID.
  2. Update the config so that silo_id is the wrong UUID.

terraform apply will work, but the wrong UUID will be written to state and, weirdly, running terraform apply again will try to create the resource as it it doesn't exist in state? I'm not sure what's going on here yet.

@sudomateo

Copy link
Copy Markdown
Collaborator Author

Here's what I did. I have this configuration.

terraform {
  required_providers {
    oxide = {
      source  = "oxidecomputer/oxide"
      version = "0.20.1"
    }
  }
}

provider "oxide" {
  profile = "colo"
}

resource "oxide_ip_pool_silo_link" "example" {
  silo_id    = "showcase"
  ip_pool_id = "c6f8c195-8cee-40b5-bc95-7eb86edec285"
  is_default = false
}

resource "oxide_subnet_pool_silo_link" "example" {
  silo_id        = "showcase"
  subnet_pool_id = "49438a11-9ded-46e2-b2be-7ae9a9fd0109"
  is_default     = false
}

I applied that using the v0.20.1 provider.

> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # oxide_ip_pool_silo_link.example will be created
  + resource "oxide_ip_pool_silo_link" "example" {
      + id         = (known after apply)
      + ip_pool_id = "c6f8c195-8cee-40b5-bc95-7eb86edec285"
      + is_default = false
      + silo_id    = "showcase"
    }

  # oxide_subnet_pool_silo_link.example will be created
  + resource "oxide_subnet_pool_silo_link" "example" {
      + id             = (known after apply)
      + is_default     = false
      + silo_id        = "showcase"
      + subnet_pool_id = "49438a11-9ded-46e2-b2be-7ae9a9fd0109"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

oxide_subnet_pool_silo_link.example: Creating...
oxide_ip_pool_silo_link.example: Creating...
oxide_subnet_pool_silo_link.example: Creation complete after 1s [id=49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658]
oxide_ip_pool_silo_link.example: Creation complete after 1s [id=06ca2c1c-37a2-44e9-b421-c138a077d61c]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Now the state shows the expected v0.20.1 values.

> terraform show
# oxide_ip_pool_silo_link.example:
resource "oxide_ip_pool_silo_link" "example" {
    id         = "06ca2c1c-37a2-44e9-b421-c138a077d61c"
    ip_pool_id = "c6f8c195-8cee-40b5-bc95-7eb86edec285"
    is_default = false
    silo_id    = "showcase"
}

# oxide_subnet_pool_silo_link.example:
resource "oxide_subnet_pool_silo_link" "example" {
    id             = "49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658"
    is_default     = false
    silo_id        = "showcase"
    subnet_pool_id = "49438a11-9ded-46e2-b2be-7ae9a9fd0109"
}

Now I changed the configuration to use the wrong UUID for both resources and ran the plan again on v0.20.1.

> terraform apply
oxide_ip_pool_silo_link.example: Refreshing state... [id=06ca2c1c-37a2-44e9-b421-c138a077d61c]
oxide_subnet_pool_silo_link.example: Refreshing state... [id=49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform planned the following actions, but then encountered a problem:

  # oxide_subnet_pool_silo_link.example must be replaced
-/+ resource "oxide_subnet_pool_silo_link" "example" {
      ~ id             = "49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658" -> (known after apply)
      ~ silo_id        = "showcase" -> "a705588a-3e62-4c19-a227-c073476c33fc" # forces replacement
        # (2 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Error: Missing resource
│
│   with oxide_ip_pool_silo_link.example,
│   on main.tf line 14, in resource "oxide_ip_pool_silo_link" "example":
│   14: resource "oxide_ip_pool_silo_link" "example" {
│
│ Unable to find requested link between IP pool c6f8c195-8cee-40b5-bc95-7eb86edec285 and silo showcase
╵

This error is expected since v0.20.1 of the provider uses a bogus id for the oxide_ip_pool_silo_link resource, which Terraform doesn't know how to handle. The oxide_subnet_pool_silo_link resource is fine since it already uses SUBNET_POOL_ID/SILO_ID as its id.

Now I update the provider to the development version using the changes in this patch and plan again.

> terraform apply
╷
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│  - oxidecomputer/oxide in /Users/ms/Projects/Oxide/terraform-provider-oxide/bin
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published
│ releases.
╵
oxide_ip_pool_silo_link.example: Refreshing state... [id=06ca2c1c-37a2-44e9-b421-c138a077d61c]
oxide_subnet_pool_silo_link.example: Refreshing state... [id=49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # oxide_ip_pool_silo_link.example will be updated in-place
  ~ resource "oxide_ip_pool_silo_link" "example" {
        id         = "c6f8c195-8cee-40b5-bc95-7eb86edec285/338bfaf2-75d3-4a1c-8850-32173960f658"
      ~ silo_id    = "showcase" -> "a705588a-3e62-4c19-a227-c073476c33fc"
        # (2 unchanged attributes hidden)
    }

  # oxide_subnet_pool_silo_link.example will be updated in-place
  ~ resource "oxide_subnet_pool_silo_link" "example" {
      ~ id             = "49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658" -> (known after apply)
      ~ silo_id        = "showcase" -> "a705588a-3e62-4c19-a227-c073476c33fc"
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Both resources show the correct refreshed id attribute using UUIDs from the SiloView change I pushed. They also show they will be updated in place with another silo's ID since that's what I put in my configuration. This is also correct.

If I don't apply this and instead change the configuration to use the correct UUID for the silo, I see both resources want to be updated in place as well, not replaced. This also seems correct since the configuration value has changed from the name to the UUID. The only thing I'm unsure about is why the oxide_subnet_pool_silo_link's id attribute shows (known after apply) when the oxide_ip_pool_silo_link resource doesn't.

> terraform apply
╷
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│  - oxidecomputer/oxide in /Users/ms/Projects/Oxide/terraform-provider-oxide/bin
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published
│ releases.
╵
oxide_ip_pool_silo_link.example: Refreshing state... [id=06ca2c1c-37a2-44e9-b421-c138a077d61c]
oxide_subnet_pool_silo_link.example: Refreshing state... [id=49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # oxide_ip_pool_silo_link.example will be updated in-place
  ~ resource "oxide_ip_pool_silo_link" "example" {
        id         = "c6f8c195-8cee-40b5-bc95-7eb86edec285/338bfaf2-75d3-4a1c-8850-32173960f658"
      ~ silo_id    = "showcase" -> "338bfaf2-75d3-4a1c-8850-32173960f658"
        # (2 unchanged attributes hidden)
    }

  # oxide_subnet_pool_silo_link.example will be updated in-place
  ~ resource "oxide_subnet_pool_silo_link" "example" {
      ~ id             = "49438a11-9ded-46e2-b2be-7ae9a9fd0109/338bfaf2-75d3-4a1c-8850-32173960f658" -> (known after apply)
      ~ silo_id        = "showcase" -> "338bfaf2-75d3-4a1c-8850-32173960f658"
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

@sudomateo

Copy link
Copy Markdown
Collaborator Author

Ah, the (known after apply) is because the id attribute for oxide_ip_pool_silo_link has the stringplanmodifier.UseStateForUnknown() plan modifier while oxide_subnet_pool_silo_link doesn't. Since the id for both resources depends on the silo they are linked to we can either remove the plan modifier from both or add it to both.

Attributes that expect UUIDs now validate whether their values are,
in fact, valid UUIDs. This catches cases where names were passed to
attributes accidentally worked.

Closes SSE-347.
@sudomateo
sudomateo force-pushed the sudomateo/vroqvovxkyro branch from 170d98e to 0ea74a4 Compare July 7, 2026 01:30
@lgfa29

lgfa29 commented Jul 7, 2026

Copy link
Copy Markdown
Member

If I don't apply this and instead change the configuration to use the correct UUID for the silo

I am applying with the wrong UUID, maybe that's the difference between our tests?

For reference, this is the config file I'm using:

terraform {
  required_version = ">= 1.11"

  required_providers {
    oxide = {
      source  = "oxidecomputer/oxide"
      version = "0.20.1"
    }
  }
}

provider "oxide" {
  host  = "http://localhost:12220"
  token = trimspace(file("${path.root}/../acctest/oxide-token"))
}

data "oxide_silo" "silo" {
  name = "test-suite-silo"
}

resource "oxide_ip_pool" "pool" {
  description = "a test ip_pool"
  name        = "test"
  ranges = [
    {
      first_address = "172.20.15.234"
      last_address  = "172.20.15.237"
    }
  ]
}

resource "oxide_ip_pool_silo_link" "link" {
  silo_id = data.oxide_silo.silo.name
  #silo_id    = "c7d7c06c-2481-4fae-a42f-59048ac54b5c"
  ip_pool_id = oxide_ip_pool.pool.id
  is_default = false
}

Then I do this:

  1. terraform apply with v0.20.1.
  2. swap the commented silo_id to use the bogus UUID.
  3. update ~/.terraformrc to use the dev_override for the binary built from this branch.
  4. terraform apply and confirm, no errors.
  5. check state file (bogus UUID in state).
  6. terraform apply again. Terraform will try to create the oxide_ip_pool_silo_link as if it's a new resource.

@sudomateo

Copy link
Copy Markdown
Collaborator Author

Ah I see it now. Without making API calls in the plan modifier I think the play here is to update Update to fail when the requested silo ID doesn't match state. The only attribute that's even updatable is is_default anyway.

Updated the logic for `oxide_ip_pool_silo_link` and
`oxide_subnet_pool_silo_link` to resolve the silo name within the `id`
and `silo_id` attributes to the silo ID. This upgrades the state to the
now validated UUID.
@sudomateo

Copy link
Copy Markdown
Collaborator Author

Alright now this does the needful.

  1. Going from name to incorrect silo ID will appear as a replacement since that's the action the user took in their configuration.
  2. Going from name to correct silo ID will result in no-op.

@lgfa29 lgfa29 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Double approve, it seems to be covering all the weird upgrade cases I've found 👍 👍

And I'm glad the final fix was rather clean, no plan modifier or weird state handling logic.

It could be useful to have a little blurb in the version upgrade guide about replacing names for UUIDs, but not strictly necessary since I think the error message is clear enough and most *_id fields are probably in the right shape already, so I leave it up to you.

Thanks for fixing this!

Comment thread .changelog/0.21.0.toml Outdated
Comment thread internal/provider/ip_pool_silo_link/resource.go
@sudomateo
sudomateo merged commit c9bd498 into main Jul 7, 2026
6 checks passed
@sudomateo
sudomateo deleted the sudomateo/vroqvovxkyro branch July 7, 2026 16:27
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.

2 participants