Skip to content

Conversation

@jmcarp
Copy link
Contributor

@jmcarp jmcarp commented Jan 16, 2026

Update to use oxidecomputer/oxide.go#359 to work through how the changes will affect the provider. Marking as a draft while we finish the upstream PR. Overall, some of the code got more verbose, because we have to switch over all known variant types, but we also dropped the json marshal/unmarshal hack, and type safety is improved.


Pull request checklist

  • Add changelog entry for this change.

@jmcarp jmcarp force-pushed the jmcarp/interface-types branch from 75aa0cd to c82f79e Compare January 16, 2026 18:42
continue
}
ip = stack.Ip
if v4, ok := nic.IpStack.Value.(*oxide.PrivateIpStackV4); ok {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that we can test the type of nic.IpStack.Value here rather than looking at the string value of nic.IpStack.Type. The behavior is the same, but we no longer have to rely on the assumption of a correspondence between type strings and types, and don't have to check errors from type casts.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like this much better but am slightly struggling to understand why we can use the type assertion here at the top level rather than needing to create helper functions like shown in https://github.com/oxidecomputer/terraform-provider-oxide/pull/598/changes#r2700298578. This might just be late Friday brain where I'm missing something small.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a bit of a weird case because we're only handling the v4 case here, not v6 or dual-stack. Those will come soon, as part of our r18 compat work. So later on we'll also have to handle all possible variants here.

return ips
}

func ipStackAsIPv4Stack(ipStack oxide.PrivateIpStack) (oxide.PrivateIpv4Stack, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the kind of hack we can dispense with using interface types. When we represent tagged unions as any, we can't cast values to the correct struct type, because they're unmarshalled to a map[string]any. Instead, we've been marshalling to json and unmarshalling to the struct type. We don't have to live like this!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, be gone!

@jmcarp jmcarp force-pushed the jmcarp/interface-types branch from c82f79e to 8a7d8aa Compare January 16, 2026 19:03
Copy link
Collaborator

@sudomateo sudomateo left a comment

Choose a reason for hiding this comment

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

This looks much better! I have a comment about the need for the helper functions and whether we can consolidate those in to the Go SDK since we'll likely need those across downstream consumers.

Comment on lines 266 to 304
// routeDestinationValue extracts the string value from a RouteDestination variant.
func routeDestinationValue(dest oxide.RouteDestination) (string, error) {
switch v := dest.Value.(type) {
case *oxide.RouteDestinationIp:
return v.Value, nil
case *oxide.RouteDestinationIpNet:
if s, ok := v.Value.(string); ok {
return s, nil
}
return fmt.Sprintf("%v", v.Value), nil
case *oxide.RouteDestinationVpc:
return string(v.Value), nil
case *oxide.RouteDestinationSubnet:
return string(v.Value), nil
default:
return "", fmt.Errorf("unknown route destination type: %T", dest.Value)
}
}

// routeTargetValue extracts the string value from a RouteTarget variant.
// Returns empty string for "drop" targets which have no value.
func routeTargetValue(target oxide.RouteTarget) (string, error) {
switch v := target.Value.(type) {
case *oxide.RouteTargetIp:
return v.Value, nil
case *oxide.RouteTargetVpc:
return string(v.Value), nil
case *oxide.RouteTargetSubnet:
return string(v.Value), nil
case *oxide.RouteTargetInstance:
return string(v.Value), nil
case *oxide.RouteTargetInternetGateway:
return string(v.Value), nil
case *oxide.RouteTargetDrop:
return "", nil
default:
return "", fmt.Errorf("unknown route target type: %T", target.Value)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this code be put in the SDK behind some Value method or similar? I understand that it'll only really work if all the variants are the same type, as these are, and other values that have different types would need to return an interface and have type assertions. Just trying to see how we can balance the boilerplate between the Go SDK and its consumers. I don't dislike this code, I just know it'll be something we use across consumers.

continue
}
ip = stack.Ip
if v4, ok := nic.IpStack.Value.(*oxide.PrivateIpStackV4); ok {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like this much better but am slightly struggling to understand why we can use the type assertion here at the top level rather than needing to create helper functions like shown in https://github.com/oxidecomputer/terraform-provider-oxide/pull/598/changes#r2700298578. This might just be late Friday brain where I'm missing something small.

return ips
}

func ipStackAsIPv4Stack(ipStack oxide.PrivateIpStack) (oxide.PrivateIpv4Stack, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, be gone!

@jmcarp jmcarp force-pushed the jmcarp/interface-types branch from 8a7d8aa to 265fa21 Compare January 21, 2026 15:12
Type: types.StringValue(string(vpcRouterRoute.Destination.Type)),
Value: types.StringValue(vpcRouterRoute.Destination.Value.(string)),
Type: types.StringValue(string(vpcRouterRoute.Destination.Type())),
Value: types.StringValue(vpcRouterRoute.Destination.String()),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: we added String() methods to a select set of api types, all of whose variants either wrap or are type-defined to string.

}

// newRouteDestination creates a RouteDestination from type and value strings.
func newRouteDestination(typeStr, value string) (oxide.RouteDestination, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could think about moving these constructors to helpers in the sdk, like the String() methods we've already added, but this feels very specific to terraform. I'm leaving these constructors here for now.

@jmcarp
Copy link
Contributor Author

jmcarp commented Jan 21, 2026

I'm pretty happy with these changes now. We don't actually have a huge number of oneOf types in the api, so the changes only touch a few files in the terraform provider. The changes fall into a few categories:

  • Trivial changes, like s/Type/Type()/.
  • New constructors for oneOf types when preparing the body of an api request.
  • Error handling for those constructors, since they can return errors if we encounter an unexpected variant type.

@jmcarp
Copy link
Contributor Author

jmcarp commented Jan 22, 2026

Updated to use new String() and As$Variant() helpers in oxide.go.

@jmcarp jmcarp marked this pull request as ready for review January 22, 2026 18:16
@jmcarp jmcarp requested a review from a team as a code owner January 22, 2026 18:16
Copy link
Member

@lgfa29 lgfa29 left a comment

Choose a reason for hiding this comment

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

Changes look like a nice improvement. I will rebase my WIP branches once this is merged to get a hands-on experience with the new SDK.

I also wondered about the generators, but couldn't think of a good way to handle them. I wouldn't say they're Terraform specific, but this case is somewhat particular because all variants have a similar structure, so it's easy to create a New*(string, string) function. If any of the variants were a little different, the constructor would start to become quite messy (like having arguments that are only used with specific types).

}
ip = stack.Ip
if v4, ok := nic.IpStack.AsV4(); ok {
ip = v4.Value.Ip
Copy link
Member

Choose a reason for hiding this comment

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

We should probably keep the error handling in an else clause. This part of the code will probably change when we add IPv6 support, but keeping the error is a good reminder to double-check other conditions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Which error handling? Do you mean we should log an error if we got a type other than oxide.PrivateIpStackTypeV4 for now?

Copy link
Member

Choose a reason for hiding this comment

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

Yup. Mostly just a gatekeeper to make sure we parse the IP properly when implementing IPv6. Without the error the instance IP may be set to an empty string in state .

Comment on lines -1169 to -1178
stack, err := ipStackAsIPv4Stack(nic.IpStack)
if err != nil {
diags.AddError(
"Unable to read instance network interface:",
"Error: "+err.Error(),
)
continue
}
n.IPAddr = types.StringValue(stack.Ip)

Copy link
Member

Choose a reason for hiding this comment

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

Neat! 🧹

Comment on lines 709 to 738
Targets: newTargetsModelFromResponse(rule.Targets),
Targets: targets,
Copy link
Member

Choose a reason for hiding this comment

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

I'm necessarily opposed to the change, but it seems unnecessary in this case since the response from newTargetsModelFromResponse() is not being processed?

}

filters, diags := newFiltersModelFromResponse(rule.Filters)
diags.Append(diags...)
Copy link
Member

Choose a reason for hiding this comment

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

Ops, good catch. I wonder if error messages were being duplicated because of this.

Comment on lines 899 to 900
switch typeStr {
case "vpc":
Copy link
Member

Choose a reason for hiding this comment

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

I think that, in general, the provider tries to use SDK values instead of direct strings, so this part would be something like:

	switch oxide.VpcFirewallRuleHostFilterType(typeStr) {
	case oxide.VpcFirewallRuleHostFilterTypeVpc :

Comment on lines 693 to 697
return nil, fmt.Errorf("creating filters for rule %q: %w", ruleName, err)
}
targets, err := newTargetTypeFromModel(rule.Targets)
if err != nil {
return nil, fmt.Errorf("creating targets for rule %q: %w", ruleName, err)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return nil, fmt.Errorf("creating filters for rule %q: %w", ruleName, err)
}
targets, err := newTargetTypeFromModel(rule.Targets)
if err != nil {
return nil, fmt.Errorf("creating targets for rule %q: %w", ruleName, err)
return nil, fmt.Errorf("error creating filters for rule %q: %w", ruleName, err)
}
targets, err := newTargetTypeFromModel(rule.Targets)
if err != nil {
return nil, fmt.Errorf("error creating targets for rule %q: %w", ruleName, err)

Minor suggestion. These error messages read like log lines to me.

// newRouteDestination creates a RouteDestination from type and value strings.
func newRouteDestination(typeStr, value string) (oxide.RouteDestination, error) {
switch typeStr {
case "ip":
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as above wrt using SDK values instead of strings.

@jmcarp
Copy link
Contributor Author

jmcarp commented Jan 23, 2026

I also wondered about the generators, but couldn't think of a good way to handle them. I wouldn't say they're Terraform specific, but this case is somewhat particular because all variants have a similar structure, so it's easy to create a New*(string, string) function. If any of the variants were a little different, the constructor would start to become quite messy (like having arguments that are only used with specific types).

I think it would be fine to add these constructors specifically for oneOfs whose variants are all string-like, similar to the String() methods in https://github.com/oxidecomputer/oxide.go/blob/main/oxide/helpers.go. Let me just add those while we're thinking about it.

@jmcarp jmcarp force-pushed the jmcarp/interface-types branch from aed9781 to f90f8a9 Compare January 27, 2026 15:37
Now that oxide.go uses interfaces to represent certain oneOf types in nexus,
bump the sdk to latest, and update uses of those oneOf types.
* Error on ipv6 until supported.
* Fix merge conflicts.
* Run golangci-lint.
@jmcarp jmcarp force-pushed the jmcarp/interface-types branch from f90f8a9 to fe90d36 Compare January 27, 2026 15:38
@jmcarp jmcarp merged commit b3fd554 into main Jan 27, 2026
3 checks passed
@jmcarp jmcarp deleted the jmcarp/interface-types branch January 27, 2026 16:04
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