-
Notifications
You must be signed in to change notification settings - Fork 12
Bump oxide.go and handle interface types. #598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
75aa0cd to
c82f79e
Compare
| continue | ||
| } | ||
| ip = stack.Ip | ||
| if v4, ok := nic.IpStack.Value.(*oxide.PrivateIpStackV4); ok { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, be gone!
c82f79e to
8a7d8aa
Compare
sudomateo
left a comment
There was a problem hiding this 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.
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, be gone!
8a7d8aa to
265fa21
Compare
| 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()), |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
|
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:
|
265fa21 to
aed9781
Compare
|
Updated to use new |
lgfa29
left a comment
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 .
| 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) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! 🧹
| Targets: newTargetsModelFromResponse(rule.Targets), | ||
| Targets: targets, |
There was a problem hiding this comment.
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...) |
There was a problem hiding this comment.
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.
| switch typeStr { | ||
| case "vpc": |
There was a problem hiding this comment.
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 :| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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": |
There was a problem hiding this comment.
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.
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. |
aed9781 to
f90f8a9
Compare
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.
f90f8a9 to
fe90d36
Compare
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
switchover all known variant types, but we also dropped the json marshal/unmarshal hack, and type safety is improved.Pull request checklist