-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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 IPv6 support for the destination controller #12428
Conversation
322eafb
to
9af27f1
Compare
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.
Are the address families of a service mutable? Is there an ordering concern here where we get an ipv4 endpoint slice update first, then the ipv6 endpoint slice (but the entires from the prior endpoint slice are preferred) and then we get the svc update that tells us that the service is ipv6?
Ideally we could make this more robust to update ordering and eventually consistent. Could we store two address maps in an AddressSet (one for ipv4 addresses and one for ipv6 addresses) and then have the Translator choose between the two similar to how we do for local traffic policy?
Yes, a service's I like your suggestion about having the endpoints watcher index all the IPs and have the translator filter the appropriate ones. I think we'd also have to track the service ipFamilies entry into the AddressSet. So we'd have to pull the ES' associated service from the lister for that, assuming it's already there, which is a softer assumption than the ordering one mentioned above, but a similar one we already make when we pull the associated pod object and that doesn't seem to have caused any issues. And so the translator would have all the information to do the filtering. Does that make sense? |
Yeah I agree we probably need to track
This way, even if the address mode of a service toggles back and forth, we'll always have the full set of addresses for both address families and we can just swap between them. |
Turns out adding a separate map for IPv6 addresses implies a lot of changes and added complexity. I went for an alternative approach, expanding the key in the So the translator keeps both IPv4 and IPv6 addresses in In the latest change I'm also consuming the |
|
||
remove[id] = address | ||
|
||
if id.IPFamily == corev1.IPv6Protocol && et.preferIPv6 { |
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.
Is this block saying that if the current snapshot contains both IPV6 and IPV4 versions of an address, we should add the IPV4? I don't understand 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.
It means that if we're removing an IPv6 and we already know about its IPv4 counterpart, send an Add event for the IPv4. This would happen for example when a dual-stack service is updated to be single-stack IPv4; the event for deletion of the IPv6 ES is received and we'd like clients to fall back to the IPv4 ES.
continue | ||
} | ||
|
||
if id.IPFamily == corev1.IPv4Protocol && et.preferIPv6 { |
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.
Does this say that if the snapshot contains both ipv6 and ipv6 versions of an address then don't remove that address? Shouldn't this depend on what's in filtered
?
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.
In this piece we say that we shouldn't remove an IPv4 address if there's no IPv6 alternative available. You're right we should depend on filtered
instead.
} | ||
t.Run("Returns endpoints (dual-stack)", func(t *testing.T) { | ||
addr := fmt.Sprintf("[%s]:%d", podIPv6Dual, port) | ||
testReturnEndpoints(t, fullyQualifiedNameDual, addr) | ||
}) | ||
|
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.
Can we also add a test for the interaction between zone filtering and dual-stack?
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.
Will certainly do.
@@ -339,19 +343,69 @@ func (et *endpointTranslator) filterAddresses() watcher.AddressSet { | |||
func (et *endpointTranslator) diffEndpoints(filtered watcher.AddressSet) (watcher.AddressSet, watcher.AddressSet) { | |||
add := make(map[watcher.ID]watcher.Address) | |||
remove := make(map[watcher.ID]watcher.Address) |
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 updated diffing logic is complex and difficult to think through all the cases and convince ourselves that we didn't miss any. I think this could be simplified by breaking it into two discrete steps:
- A
func selectAddressFamily(addresses watcher.AddressSet) watcher.AddressSet
which filters the address set based on address family:
- if enable-ipv6 is disabled, drop all ipv6 addresses
- if the address set contains both versions of an address, drop the ipv4 one
- This will result in the filtered address set being the final desired state, which means we can just let
diffEndpoints
do it's thing and diff against the snapshot without being specifically aware of address family. it should generate the diff necessary to bring the filtered snapshot to the desired state.
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.
The expanded diffing logic was also trying to avoid generating spurious messages, like a Remove that didn't correspond to any previous Add, _but I may be over complicating things. Let me experiment with your proposed approach and see how it goes. I should also include the additional unit tests first to be more confident about these changes.
81e1b4e
to
cda9647
Compare
Services in dual-stack mode result in the creation of two EndpointSlices, one for each IP family. Before this change, the Get Destination API would nondeterministically return the address for any of those ES, depending on which one was processed last by the controller because they would overwrite each other. As part of the ongoing effort to support IPv6/dual-stack networks, this change fixes that behavior giving preference to IPv6 addresses whenever a service exposes both families. Also the server unit tests were updated to segregate the tests and resources dealing with the IPv4/IPv6/dual-stack cases.
cda9647
to
f30d952
Compare
Ok I've replaced Also there are a new set of unit tests in |
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.
Nice!
Services in dual-stack mode result in the creation of two EndpointSlices, one for each IP family. Before this change, the Get Destination API would nondeterministically return the address for any of those ES, depending on which one was processed last by the controller because they would overwrite each other.
As part of the ongoing effort to support IPv6/dual-stack networks, this change fixes that behavior giving preference to IPv6 addresses whenever a service exposes both families.
We introduce a new field
ipv6Only
for theservicePublisher
struct that is set totrue
for any service that exposes an IPv6 address. Whenever an ES is added or updated, ifipv6Only
is true, it's ignored if its addressType is IPv4.Also the server unit tests were updated to segregate the tests and resources dealing with the IPv4/IPv6/dual-stack cases.