-
Notifications
You must be signed in to change notification settings - Fork 373
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 controller to antrea-agent for implementing Egress #2026
Conversation
@jianjuns I haven't rebased on main branch and added any unit test. Create this PR for you to review the main code and the e2e code earlier. Please ignore changes other than the below files:
I will keep updating while you are reviewing. |
egressIPStates: map[string]*egressIPState{}, | ||
egressBindings: map[string]*egressBinding{}, | ||
localIPDetector: &localIPDetector{}, | ||
idAllocator: newIDAllocator(256), |
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.
Define a constant for the maximum number of SNAT IPs. I feel maybe 100 is good enough.
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 do. But since we allocate 8 bits for the mark, no harm to allow 255 IPs?
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 only thing is it consumes more mem.
|
||
type localIPDetector struct{} | ||
|
||
func (d *localIPDetector) IsLocalIP(ip string) (bool, 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 can start from this, but later we should add periodic IP detection for the case SNAT IP is added after Egress is received.
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.
Sure, I thought about it. We could use "ip monitor address" to watch the IP Address event so that we don't need to list addresses everytime we check if an IP is local or not, and don't need to do polling for IP change.
Will add a TODO if I can't make it in this release.
} | ||
} | ||
// Ensure datapath is configured properly. | ||
if !ipState.datapathInstalled { |
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.
So, datapathInstalled is just to catch the previous failures in flow/rule installation?
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, and to avoid extra external calls in later syncs.
if err := c.ofClient.InstallSNATMarkFlows(ipState.egressIP, ipState.mark); err != nil { | ||
return 0, fmt.Errorf("error installing SNAT mark flows for IP %s: %v", ipState.egressIP, err) | ||
} | ||
if err := c.routeClient.AddSNATRule(ipState.egressIP, ipState.mark); err != nil { |
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 it a problem to call InstallSNATMarkFlows() again in retry, after AddSNATRule() fails?
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 thought adding same openflow rule is idempotent, no? or I could have two flags to record state of snat rule and iptables rule separately.
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.
OVS side should be idempotent. But how about our flow cache?
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 updating flow cache just overrides it so should be ok. Anyway I switched to 2 separate flags to not have assumption on the implementation.
|
||
// getOrAllocateMark gets or allocates a mark for a local Egress. Multiple Egresses can share same Egress IP. | ||
// The mark is per Egress IP, not per Egress. | ||
func (c *Controller) getOrAllocateMark(egressName, egressIP string) (uint32, 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 func also installs flows and iptables rule. Call it realizeEgress?
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 do
return true | ||
} | ||
|
||
// getOrAllocateMark gets or allocates a mark for a local Egress. Multiple Egresses can share same Egress 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 -> the same
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.
If we install flows/iptables rule in the func, add comments about these.
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 do
} | ||
// Ensure datapath is configured properly. | ||
if !ipState.datapathInstalled { | ||
if err := c.ofClient.InstallSNATMarkFlows(ipState.egressIP, ipState.mark); err != nil { |
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 we install flows / rule in the caller with the lock released?
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 may introduce extra external calls. For example, if two new Egresses have same Egress IP, and the first one doesn't configure the datapath with the lock, the other one will also try to configure the datapath, unless we introduce per-IP locks. However, even if we add per-IP locks, at least iptables calls will be serial anyway. Do you think it's worthwhile?
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 per-IP lock will be necessary as there could be race condition when an Egress starts to use an EgressIP while another Egress stops to use the same EgressIP.
return nil | ||
} | ||
if ipState.mark != 0 { | ||
if err := c.ofClient.UninstallSNATMarkFlows(ipState.mark); err != nil { |
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 here - can we release lock before uninstall flows/rule?
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.
ditto
return nil | ||
} | ||
|
||
func (c *Controller) getEgressState(egressName string) (*egressState, bool) { |
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 assume you have considered this, but just a remainder - make sure access to the returned EgressState without lock, will not create any sync issue.
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, I have considered this. The mutex is to protect the map, not the egressState item. The workqueue guarantees an Egress will only be processed by a single worker at any time. So the returned EgressState has no race condition. Maybe I could add this in the comment to be clear.
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.
Comments will be good.
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.
Added to the comment of the mutex.
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.
@jianjuns thanks for the review
|
||
type localIPDetector struct{} | ||
|
||
func (d *localIPDetector) IsLocalIP(ip string) (bool, 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.
Sure, I thought about it. We could use "ip monitor address" to watch the IP Address event so that we don't need to list addresses everytime we check if an IP is local or not, and don't need to do polling for IP change.
Will add a TODO if I can't make it in this release.
egressIPStates: map[string]*egressIPState{}, | ||
egressBindings: map[string]*egressBinding{}, | ||
localIPDetector: &localIPDetector{}, | ||
idAllocator: newIDAllocator(256), |
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 do. But since we allocate 8 bits for the mark, no harm to allow 255 IPs?
return true | ||
} | ||
|
||
// getOrAllocateMark gets or allocates a mark for a local Egress. Multiple Egresses can share same Egress 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.
will do
|
||
// getOrAllocateMark gets or allocates a mark for a local Egress. Multiple Egresses can share same Egress IP. | ||
// The mark is per Egress IP, not per Egress. | ||
func (c *Controller) getOrAllocateMark(egressName, egressIP string) (uint32, 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.
will do
} | ||
} | ||
// Ensure datapath is configured properly. | ||
if !ipState.datapathInstalled { |
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, and to avoid extra external calls in later syncs.
} | ||
// Ensure datapath is configured properly. | ||
if !ipState.datapathInstalled { | ||
if err := c.ofClient.InstallSNATMarkFlows(ipState.egressIP, ipState.mark); err != nil { |
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 may introduce extra external calls. For example, if two new Egresses have same Egress IP, and the first one doesn't configure the datapath with the lock, the other one will also try to configure the datapath, unless we introduce per-IP locks. However, even if we add per-IP locks, at least iptables calls will be serial anyway. Do you think it's worthwhile?
if err := c.ofClient.InstallSNATMarkFlows(ipState.egressIP, ipState.mark); err != nil { | ||
return 0, fmt.Errorf("error installing SNAT mark flows for IP %s: %v", ipState.egressIP, err) | ||
} | ||
if err := c.routeClient.AddSNATRule(ipState.egressIP, ipState.mark); err != nil { |
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 thought adding same openflow rule is idempotent, no? or I could have two flags to record state of snat rule and iptables rule separately.
return nil | ||
} | ||
if ipState.mark != 0 { | ||
if err := c.ofClient.UninstallSNATMarkFlows(ipState.mark); err != nil { |
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.
ditto
return nil | ||
} | ||
|
||
func (c *Controller) getEgressState(egressName string) (*egressState, bool) { |
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, I have considered this. The mutex is to protect the map, not the egressState item. The workqueue guarantees an Egress will only be processed by a single worker at any time. So the returned EgressState has no race condition. Maybe I could add this in the comment to be clear.
type localIPDetector struct{} | ||
|
||
// IsLocalIP checks if the provided IP is configured on the Node. | ||
// TODO: Instead of listing all IP addresses every time, it can maintain a cache and subscribe the IP address change to |
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 need notification to controller for changes too, so controller can handle IP add/delete and update DP correctly.
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 found golang API to watch ip address, will update this interface soon.
// Disable resyncing. | ||
resyncPeriod time.Duration = 0 | ||
// maxEgressIPsPerNode is the maximum number of Egress IPs can be configured on a Node. | ||
maxEgressIPsPerNode = 256 |
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 0 is for default IP, so the max number should be 255?
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.
Thanks, will fix. And switch to a lazy initialization mode to save memory
return nil | ||
} | ||
|
||
func (c *Controller) getEgressState(egressName string) (*egressState, bool) { |
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.
Comments will be good.
766c3bf
to
5fdb6d2
Compare
/test-all |
@tnqn : is this PR ready for final review and merge (it still has WIP in title)? |
@jianjuns It's ready for review. I have made all changes except adding the verification in CRD schema. I have to add a simple one to enable e2e tests. Do you want me to add a comprehensive one in this PR? or you want to have another PR? |
We can do another PR. |
/test-all |
Codecov Report
@@ Coverage Diff @@
## main #2026 +/- ##
==========================================
- Coverage 61.57% 55.22% -6.36%
==========================================
Files 265 268 +3
Lines 19770 20177 +407
==========================================
- Hits 12174 11142 -1032
- Misses 6333 7850 +1517
+ Partials 1263 1185 -78
Flags with carried forward coverage won't be shown. Click here to find out more.
|
The controller watches Egress and EgressGroup API and calls openflow client and route client to enforce an Egress. Co-authored-by: ceclinux <ceclinux@users.noreply.github.com>
/test-all |
@jianjuns I verified the feature on IPv6 cluster and it worked. Will make the e2e test support IPv6 with another PR. |
/test-all |
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.
Good to know IPv6 works too!
The controller watches Egress and EgressGroup API and calls openflow
client and route client to enforce an Egress.
Co-authored-by: ceclinux ceclinux@users.noreply.github.com
For #1924