Skip to content
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

Add support for LoadBalancersInboundNatRule #2984

Merged
merged 8 commits into from
May 23, 2023
Merged

Conversation

super-harsh
Copy link
Collaborator

Closes #2894

What this PR does / why we need it:

This PR adds support for LoadBalancersInboundNatRule to be created and managed separately.

Special notes for your reviewer:

Added load_balancer_extension.go to handle the edge case where InboundNatRule is present on LoadBalancer and independently.

If applicable:

  • this PR contains tests

Comment on lines +77 to +82
func getInboundNatRuleGVK(lb genruntime.ARMMetaObject) schema.GroupVersionKind {
gvk := genruntime.GetOriginalGVK(lb)
gvk.Kind = reflect.TypeOf(network.LoadBalancersInboundNatRule{}).Name() // "LoadBalancersInboundNatRule"

return gvk
}
Copy link
Member

Choose a reason for hiding this comment

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

Implementations of GetOriginalGVK() return a fully populated GroupVersionKind, e.g.:

// OriginalGVK returns a GroupValueKind for the original API version used to create the resource
func (rule *LoadBalancersInboundNatRule) OriginalGVK() *schema.GroupVersionKind {
	return &schema.GroupVersionKind{
		Group:   GroupVersion.Group,
		Version: rule.Spec.OriginalVersion(),
		Kind:    "LoadBalancersInboundNatRule",
	}
}

It's not valid to grab the GVK for one kind of object and construct a GVK by overwriting one of the fields - there's no guarantee that the resulting GVK identifies anything at all. This might happen to currently work by coincidence, but that's far from ideal.

// the hub type has been changed but this extension has not been updated to match
var _ conversion.Hub = typedObj

inboundNatRuleGVK := getInboundNatRuleGVK(obj)
Copy link
Member

Choose a reason for hiding this comment

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

The safe way to do this (see my other comment) is to construct an instance of the type you want and call OriginalGVK() on it directly.

Something like this:

	blankRule := &network.LoadBalancersInboundNatRule{}
	inboundNatRuleGVK := blankRule.GetOriginalGVK()

You may be able to combine these into one line.

Copy link
Member

@matthchr matthchr May 16, 2023

Choose a reason for hiding this comment

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

The problem is that we are injecting one type into another. Imagine for example:

LoadBalancer OriginalGVK 20230101
LoadBalancerNATRule OriginalGVK 20230601

The "contract" (if you can call it that) is that 20230101 LoadBalancer can embed 20230101 NAT rules, but it can't embed the 20230601 version because that's newer. We need to run conversion.

This has a requirement that is poorly documented here and should probably be called out (here and in the vmss and RouteTable extenions too): In each API version, there must be a version of the subresource and the parent resource (no skipping one or the other). We should probably put a comment in azure-arm.yaml to that effect as well next to these resources.

I believe if the GVK doesn't exist, the List call will fail saying no such GVK, so at runtime we're "protected" here somewhat.

The issue is you can't know statically what GVK you want, so you can't do what you suggest and get the GVK statically. We need to get the GVK dynamically based on GVK that we're going to use to generate the ARM payload.

Copy link
Member

Choose a reason for hiding this comment

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

Reiterating this bit:

This has a requirement that is poorly documented here and should probably be called out (here and in the vmss and RouteTable extenions too): In each API version, there must be a version of the subresource and the parent resource (no skipping one or the other). We should probably put a comment in azure-arm.yaml to that effect as well next to these resources.

My takeaway from @theunrepentantgeek's comment, which we could try to find a way to do, is to ensure that there really is such a version for the parent type and the child type. right now we just make the assumption. Obviously that happens to work in practice but we're not very hardened against issues with that assumption here. Some combination of:

  1. Comment in azure-arm.yaml saying that there must always be a parent and child in the same API version (to call it out for others when adding new versions)
  2. Dynamic checks via scheme lookup here to ensure that such a type actually exists.

Seems the best way out to me?

I at least would be happy to see this done in a separate PR where we tackled all 3-4 of the resources that have this requirement together, rather than trying to do them all here, but also curious what @theunrepentantgeek thinks.

Copy link
Member

Choose a reason for hiding this comment

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

Ergh. I get it now.

My key problem here is that any failure happens at runtime of the controller, while the fix requires changes to the configuration of the generator - these are very far apart in the code base, and the errors will happen at very different times. It's also more than likely the person experiencing the failure won't be the person who needs to change the configuration.

I'd suggest the following attack:

  • In this PR, add comments in azure-arm.yaml to all the afflicted resources (not just the new resources in this PR).
  • Log an issue to track the problem
  • Add config + check to the pipeline to enforce the relationship, somehow, so that we can't possibly leave things out
  • Revisit this code to see if we can find a less-brittle way to do the conversion that's required

inboundNatRule := inboundNatRule

var transformed genruntime.ARMResourceSpec
transformed, err = transformToARM(ctx, &inboundNatRule, inboundNatRuleGVK, kubeClient, resolver)
Copy link
Member

Choose a reason for hiding this comment

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

Could/Should we modify transformToARM into a generic method where you specify the type? We could avoid having to pass in the GVK by moving the logic internally.

if err != nil {
return errors.Wrap(err, "unable to check that embedded inboundNatRule is the same as inboundNatRule")
}
if string(embeddedInboundNatRuleJSON) != string(inboundNatRuleJSON) {
Copy link
Member

Choose a reason for hiding this comment

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

Does JSON serialization have a guarantee of ordering? I suspect this may be fragile.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the same logic we've been using for other resource extensions(vnet, routetable) as well, and noticed no serialization issues. Can you think of a better way to do this?

Copy link
Member

Choose a reason for hiding this comment

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

According to this stackoverflow post, which references the Go JSON serialization code, struct order will be consistent.

That's an implementation detail though and is technically subject to change.

Map ordering is not guaranteed, so if the structs had maps we could be at risk here. Since we can't know for sure that the structs don't have maps, I think in general I agree with Bevan that this is not ideal.

Possibly what we could do here is:

  1. Serialize these types to JSON
  2. Deserialize them into a generic map[string]interface{} (map of maps)
  3. use reflect.DeepEqual on those.

While the actual JSON representations of the types might differ because of ordering, I believe that reflect.DeepEqual is not ordering sensitive for maps and so using that would be safe.

As above it might make sense to merge this as-is (given it's a bug we have across all the impls right now) and fix it across all of them together for consistency.

Copy link
Member

Choose a reason for hiding this comment

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

if we do decide to fix this but in a different PR, let's file a bug for it so we don't forget. Make sure to mention RouteTable and VNETs there too.

@codecov-commenter
Copy link

Codecov Report

❗ No coverage uploaded for pull request base (main@283df38). Click here to learn what that means.
The diff coverage is 57.62%.

@@           Coverage Diff           @@
##             main    #2984   +/-   ##
=======================================
  Coverage        ?   54.19%           
=======================================
  Files           ?     1368           
  Lines           ?   579780           
  Branches        ?        0           
=======================================
  Hits            ?   314200           
  Misses          ?   213892           
  Partials        ?    51688           
Impacted Files Coverage Δ
...i/network/customizations/route_table_extensions.go 62.16% <ø> (ø)
...twork/customizations/virtual_network_extensions.go 53.84% <ø> (ø)
.../v1api20201101/load_balancer_spec_arm_types_gen.go 33.33% <ø> (ø)
...i/network/v1api20201101/load_balancer_types_gen.go 49.09% <ø> (ø)
...twork/v1api20201101/network_interface_types_gen.go 49.43% <ø> (ø)
...pi20201101/public_ip_address_spec_arm_types_gen.go 33.33% <ø> (ø)
...twork/v1api20201101/public_ip_address_types_gen.go 48.95% <ø> (ø)
...1api20201101storage/network_interface_types_gen.go 55.17% <ø> (ø)
...1api20201101storage/public_ip_address_types_gen.go 52.63% <ø> (ø)
...d_balancers_inbound_nat_rule_spec_arm_types_gen.go 33.33% <33.33%> (ø)
... and 6 more

// the hub type has been changed but this extension has not been updated to match
var _ conversion.Hub = typedObj

inboundNatRuleGVK := getInboundNatRuleGVK(obj)
Copy link
Member

Choose a reason for hiding this comment

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

Reiterating this bit:

This has a requirement that is poorly documented here and should probably be called out (here and in the vmss and RouteTable extenions too): In each API version, there must be a version of the subresource and the parent resource (no skipping one or the other). We should probably put a comment in azure-arm.yaml to that effect as well next to these resources.

My takeaway from @theunrepentantgeek's comment, which we could try to find a way to do, is to ensure that there really is such a version for the parent type and the child type. right now we just make the assumption. Obviously that happens to work in practice but we're not very hardened against issues with that assumption here. Some combination of:

  1. Comment in azure-arm.yaml saying that there must always be a parent and child in the same API version (to call it out for others when adding new versions)
  2. Dynamic checks via scheme lookup here to ensure that such a type actually exists.

Seems the best way out to me?

I at least would be happy to see this done in a separate PR where we tackled all 3-4 of the resources that have this requirement together, rather than trying to do them all here, but also curious what @theunrepentantgeek thinks.

if err != nil {
return errors.Wrap(err, "unable to check that embedded inboundNatRule is the same as inboundNatRule")
}
if string(embeddedInboundNatRuleJSON) != string(inboundNatRuleJSON) {
Copy link
Member

Choose a reason for hiding this comment

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

According to this stackoverflow post, which references the Go JSON serialization code, struct order will be consistent.

That's an implementation detail though and is technically subject to change.

Map ordering is not guaranteed, so if the structs had maps we could be at risk here. Since we can't know for sure that the structs don't have maps, I think in general I agree with Bevan that this is not ideal.

Possibly what we could do here is:

  1. Serialize these types to JSON
  2. Deserialize them into a generic map[string]interface{} (map of maps)
  3. use reflect.DeepEqual on those.

While the actual JSON representations of the types might differ because of ordering, I believe that reflect.DeepEqual is not ordering sensitive for maps and so using that would be safe.

As above it might make sense to merge this as-is (given it's a bug we have across all the impls right now) and fix it across all of them together for consistency.

if err != nil {
return errors.Wrap(err, "unable to check that embedded inboundNatRule is the same as inboundNatRule")
}
if string(embeddedInboundNatRuleJSON) != string(inboundNatRuleJSON) {
Copy link
Member

Choose a reason for hiding this comment

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

if we do decide to fix this but in a different PR, let's file a bug for it so we don't forget. Make sure to mention RouteTable and VNETs there too.

@@ -132,9 +132,17 @@ func getGeneratedStorageTypes(

// Verify we're using the hub version of VirtualNetworksSubnet in the loop below
var _ ctrlconversion.Hub = &networkstorage.VirtualNetworksSubnet{}
var _ ctrlconversion.Hub = &networkstorage.LoadBalancersInboundNatRule{}
var _ ctrlconversion.Hub = &networkstorage.RouteTablesRoute{}
Copy link
Member

Choose a reason for hiding this comment

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

minor: Why is this line (136) needed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hub version verification was missing for RouteTablesRoute so just added that here

@super-harsh super-harsh merged commit dd230cc into main May 23, 2023
9 checks passed
@super-harsh super-harsh deleted the feature/inboundNATrules branch May 23, 2023 04:27
theunrepentantgeek added a commit that referenced this pull request May 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

Feature: Support LoadBalancer InboundNATRules
4 participants