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

Adding logic to GCE ingress controller to handle multi cluster ingresses #1033

Merged
merged 3 commits into from
Sep 14, 2017

Conversation

nikhiljindal
Copy link
Contributor

@nikhiljindal nikhiljindal commented Jul 27, 2017

Ingress controller should manage instance groups and ports for multi cluster ingresses.

Things to do:

  • Add tests
  • Verify manually that this works
  • Ensure that resources are GC'd as expected.

Sending it out now to get early feedback.

Update:
Verified that the code works as expected with the following scenarios:

Creating an ingress with gce-multi class, creates expected instance group and deleting the ingress, deletes the ig.
If there is an existing ingress, then creating gce-multi ingress, reuses the existing instance group rather than creating a new one.
There is an existing issue that deleting the ingress does not delete the corresponding named ports which is being tracked in #695.

Will add e2e tests in main repo to automate these tests.

cc @nicksardo

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Jul 27, 2017
@k8s-reviewable
Copy link

This change is Reviewable

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.03%) to 43.994% when pulling e1642581de5208fcd38b8abf4a369f36333a3066 on nikhiljindal:reportIGName into 3495bfb on kubernetes:master.

@nikhiljindal
Copy link
Contributor Author

Updated the code to add default backend service node port to the instance group and also add an annotation with the instance group names.

Verified that the code works as expected with the following scenarios:

  • Creating an ingress with gce-multi class, creates expected instance group and deleting the ingress, deletes the ig.
  • If there is an existing ingress, then creating gce-multi ingress, reuses the existing instance group rather than creating a new one.

There is an existing issue that deleting the ingress does not delete the corresponding named ports which is being tracked in #695.

Will add e2e tests in main repo to automate these tests.

cc @nicksardo PTAL

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 44.503% when pulling ab61605 on nikhiljindal:reportIGName into cf732e8 on kubernetes:master.

@nikhiljindal nikhiljindal changed the title WIP: Adding logic to GCE ingress controller to handle multi cluster ingresses Adding logic to GCE ingress controller to handle multi cluster ingresses Aug 8, 2017
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 44.712% when pulling ab61605 on nikhiljindal:reportIGName into cf732e8 on kubernetes:master.

func addInstanceGroupsAnnotation(existing map[string]string, igs []*compute.InstanceGroup) (map[string]string, error) {
if existing == nil {
existing = map[string]string{}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer asserting that the map exists outside of this func. Changing this would mean we don't need the first return variable either.

On another note, a less overloaded term than add would be nice, ie set or apply

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Renamed to set

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 44.45% when pulling 46efeb3 on nikhiljindal:reportIGName into 45e43f8 on kubernetes:master.

@@ -263,3 +265,20 @@ func addNodes(lbc *LoadBalancerController, zoneToNode map[string][]string) {
func getProbePath(p *api_v1.Probe) string {
return p.Handler.HTTPGet.Path
}

func TestAddInstanceGroupsAnnotation(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a test case for multiple zones?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


// Helper method to create instance groups.
// This method exists to ensure that we are using the same logic at all places.
func CreateInstanceGroups(nodePool NodePool, namer *utils.Namer, port int64) ([]*compute.InstanceGroup, *compute.NamedPort, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This just wraps around the namer, right?
IIUC, the namer is actually the same one.
Is it really worthy?

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 to ensure that the same logic is used at both places.
It is to prevent scenarios where one place is updated but not the other.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you still need this?

you only need to expose it at ClusterManager, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes still need it. Its also called from backends.go

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just remove this and do nodePool.AddInstanceGroup(namer.IGName(), port) in both places?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doing that risks someone changing the logic at one place without changing the other.
This is a simple wrapper method to ensure that the same code is used at both places.

// name and zone of instance groups created for the ingress.
// This is read only for users. Controller will overrite any user updates.
// This is only set for ingresses with ingressClass = "gce-multi"
instanceGroupsKey = "ingress.gcp.kubernetes.io/instance-groups"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: instanceGroupsAnnotationKey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

gceIngressClass = "gce"
ingressClassKey = "kubernetes.io/ingress.class"
gceIngressClass = "gce"
gceMultiIngressClass = "gce-multi"
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure whether the class key is settled. Maybe gce-multi-cluster is better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

@@ -342,6 +349,39 @@ func (lbc *LoadBalancerController) sync(key string) (err error) {
return syncError
}

func (lbc *LoadBalancerController) syncMultiClusterIngress(ing *extensions.Ingress, nodeNames []string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is duplicate logic here with Checkpoint. Can you refactor Checkpoint to avoid having dupplicate logic?

Currently, CreateInstanceGroup is done as part of syncing backend services. Extract create and sync instance group, and set the annotation, if gce-multi, then return early, otherwise, continue for the rest.

This requires more changes to existing logic though. I will let @nicksardo to have more input for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I dont want to call the whole Checkpoint method just to sync instance groups. I think its better to call that specific method directly here. This also ensures that the existing ingress code is independent and unchanged.

if ingExists {
ing := obj.(*extensions.Ingress)
if isGCEMultiClusterIngress(ing) {
return lbc.syncMultiClusterIngress(ing, nodeNames)
Copy link
Contributor

Choose a reason for hiding this comment

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

current me if I am wrong, if an ingress is updated from gce to gce-multi. The rest of GCP resources will stay around?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lbc.CloudClusterManager.GC above in line 303 will still be called which should delete those resources.

Still, that is an interesting case I hadnt thought of. What if we say that users can not "upgrade" an existing gce ingress to gce-multi. They will always have to delete the existing gce ingress and create a new gce-multi ingress?

@k8s-ci-robot k8s-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Aug 28, 2017
@nikhiljindal
Copy link
Contributor Author

Thanks for the review @freehan
Updated the code as per comments. PTAL

@nikhiljindal
Copy link
Contributor Author

cc @csbell

@nikhiljindal
Copy link
Contributor Author

@freehan Pushed a new commit to refactor the code to merge multi cluster sync with single cluster sync.
PTAL

var igs []*compute.InstanceGroup
var err error
for _, p := range servicePorts {
igs, _, err = instances.CreateInstanceGroups(c.instancePool, c.ClusterNamer, p.Port)
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we be appending to igs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CreateInstanceGroups creates instance groups in all zones and then adds the given named port to it.
Each time CreateInstanceGroups is called for a port, it always returns the same set of all igs. So we dont need append, just return the output of any call.

Ideally, we should call CreateInstanceGroups only the first time and then call AddNamedPort subsequent times, but that interface is not exposed yet. Will add a TODO and a comment explaining this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the comment and TODO

@nikhiljindal
Copy link
Contributor Author

Rebased to resolve conflicts

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 43.684% when pulling 0f756ae on nikhiljindal:reportIGName into 7434c50 on kubernetes:master.

Copy link
Contributor

@freehan freehan left a comment

Choose a reason for hiding this comment

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

LGTM overall, some nits. @nicksardo for a final review.


// Helper method to create instance groups.
// This method exists to ensure that we are using the same logic at all places.
func CreateInstanceGroups(nodePool NodePool, namer *utils.Namer, port int64) ([]*compute.InstanceGroup, *compute.NamedPort, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you still need this?

you only need to expose it at ClusterManager, right?

@@ -289,6 +304,17 @@ func ListAll(store cache.Store, selector labels.Selector, appendFn cache.AppendF
func (s *StoreToIngressLister) List() (ing extensions.IngressList, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

let us refactor here a little.

Refactor the current StoreToIngressLister.List() function into ListGCEIngress() and ListGCEMultiClusterIngress.

Change the return value from extensions.IngressList to []*extensions.Ingress.

Change the input value for GCETranslator.toNodePorts() to use []*extensions.Ingress

Copy link
Contributor Author

Choose a reason for hiding this comment

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

List is also used at other places. I dont want all callers to get gce and gceMulti ingresses first and then append, so retaining the List method.

ListGCEMultiIngresses will not be used anywhere so not required atm.

Copy link
Contributor

Choose a reason for hiding this comment

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

List is also used in ListRuntimeInfo. Then lbs is passed into Checkpoint. Then you filter lbs to singleClusterLbs in Checkpoint.

Please just use ListGCEIngresses in ListRuntimeInfo and remove the filtering.

Copy link
Contributor

@freehan freehan Sep 11, 2017

Choose a reason for hiding this comment

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

I would prefer changing List to something else. Like ListAll. Next time, when someone tries to use it, it would not get confused and leave a bug that only triggers when there are a combination of gce and multi-cluster ingresses.

Also, change the comment to explicitly say GCE ingress and multi-cluster ingress

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.
Renamed List to ListAll and updated the comment to be explicit.
Also renamed ListRuntimeInfo to ListGCERuntimeInfo and added a comment stating that it returns runtimeinfo only for gce ingresses and not for multi cluster ingresses.
Also removed the IsMultiCluster field from RuntimeInfo and updated Checkpoint accordingly.

@@ -279,7 +280,13 @@ func (lbc *LoadBalancerController) sync(key string) (err error) {
if err != nil {
return err
}
singleClusterIngresses, err := lbc.ingLister.ListGCEIngresses()
Copy link
Contributor

Choose a reason for hiding this comment

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

gceIngresses, err := lbc.ingLister.LIstGCEIngress()
gceMutliIngresses, err := lbc.ingLister.ListGCEMultiClusterIngress()
combinedIngresses := append(gceIngresses, gceMutliIngresses)

gceIngressNodeports := lbc.tr.toNodePorts(gceIngresses)
combinedNodeports := lbc.tr.toNodePorts(combinedIngresses)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed to gceIngresses, gceNodePorts and allIngresses, allNodePorts.

@nikhiljindal
Copy link
Contributor Author

Thanks @freehan Updated code as per comments.
PTAL.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 44.231% when pulling 8cd2902924e997c112fd18a35ee67174d5b9ab41 on nikhiljindal:reportIGName into 18ea2f7 on kubernetes:master.

@nikhiljindal
Copy link
Contributor Author

Updated code as per comments.
PTAL.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 44.19% when pulling 0f4f5c9 on nikhiljindal:reportIGName into 18ea2f7 on kubernetes:master.

@freehan
Copy link
Contributor

freehan commented Sep 11, 2017

LGTM

@nikhiljindal
Copy link
Contributor Author

Pushed a new commit to call CreateInstanceGroups only once instead of thrice before. Its an extra optimization, should not have any user visible impact (except maybe get a bit faster).

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 44.234% when pulling 32f311d on nikhiljindal:reportIGName into 18ea2f7 on kubernetes:master.

if err != nil {
return err
var err error
if igs == nil {
Copy link
Contributor

@nicksardo nicksardo Sep 12, 2017

Choose a reason for hiding this comment

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

Suggest adding comment saying when igs is nil.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

func (c *ClusterManager) SyncNodesInInstanceGroups(nodeNames []string) error {
if err := c.instancePool.Sync(nodeNames); err != nil {
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the point of this wrapper?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes had added it when the same code was being called from 2 places. Not required now after the latest refactor. Removed.

lbNames := lbc.ingLister.Store.ListKeys()
lbs, err := lbc.ListRuntimeInfo()
lbs, err := lbc.ListGCERuntimeInfo()
Copy link
Contributor

@nicksardo nicksardo Sep 12, 2017

Choose a reason for hiding this comment

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

IMO, another nice change would be to plumb gceIngresses to lbc.ListGCERuntimeInfo() and maybe rename it to lbc.ToGCERuntimeInfo(gceIngresses). There would be no performance increase from this change, but the code would be clearer to follow and the function would appear less magical. Up to you.

Copy link
Contributor Author

@nikhiljindal nikhiljindal Sep 13, 2017

Choose a reason for hiding this comment

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

Yes its definitely cleaner!
toRuntimeInfo does not need any special comments or logic about getting gce ingresses only or multi cluster ingresses as well. It just converts the ingresses that it receives. So thats nice!

Copy link
Contributor Author

@nikhiljindal nikhiljindal Sep 13, 2017

Choose a reason for hiding this comment

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

Also named it toRuntimeInfo instead of toGCERuntimeInfo since it now has no single or multi cluster specific logic. Its a generic method that returns RuntimeInfo structs for the given ingress objests

// Record any errors during sync and throw a single error at the end. This
// allows us to free up associated cloud resources ASAP.
if err := lbc.CloudClusterManager.Checkpoint(lbs, nodeNames, nodePorts); err != nil {
if igs, err = lbc.CloudClusterManager.Checkpoint(lbs, nodeNames, gceNodePorts, allNodePorts); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since igs isn't being set anywhere else. I'd recommend killing the instantiation on ln 323 and breaking apart this line into two.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -329,21 +337,36 @@ func (lbc *LoadBalancerController) sync(key string) (err error) {
if !ingExists {
return syncError
}
ing := obj.(*extensions.Ingress)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, the dereference copy may have been on purpose from the original author. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm I dont see a reason why but am not confident enough yet. Reverted for now. Will look closer and probably change it in a different PR

continue
}
knownPorts = append(knownPorts, port)
return knownPorts
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and line 524: why early return? Original code had continues.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have extracted an ingressToNodePorts method out that returns node ports for a single ingress.
toNodePorts calls ingressToNodePorts multiple times for each ingress.

Before this change, it was all a single method and hence we were using continue to move on to next ingress if there was an error with an ingress.
With the new method, ingressToNodePorts returns early in case of error and toNodePorts then calls it again with the next ingress.
So the behavior should continue to be same.

Copy link
Contributor

Choose a reason for hiding this comment

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

The original behavior was a continue of the local loop, not the outer loop, which means it would continue to the next path or rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

of course, I feel silly!
Fixed it now.

@@ -640,3 +673,34 @@ func (o PodsByCreationTimestamp) Less(i, j int) bool {
}
return o[i].CreationTimestamp.Before(o[j].CreationTimestamp)
}

// setInstanceGroupsAnnotation sets the instance-groups annotation with names of the given instance groups.
func setInstanceGroupsAnnotation(existing map[string]string, igs []*compute.InstanceGroup) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Recommend naming something multi-cluster specific

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The method does not have any multi cluster specific logic. Its a generic method that sets the instance groups annotation. Hence I am inclined to keep the name generic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay

Name string
Zone string
}
instanceGroups := []Value{}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: var instanceGroups []Value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

for _, p := range nodePorts {
portMap[p.Port] = p
}
nodePorts = []backends.ServicePort{}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: either var nodePorts []backends.ServicePort or nodePorts := make([]backends.ServicePort, 0 len(portMap))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to nodePorts = make([]backends.ServicePort, 0 len(portMap))

return igs, nil
}

func (c *ClusterManager) CreateInstanceGroups(servicePorts []backends.ServicePort) ([]*compute.InstanceGroup, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rename this and the other CreateInstanceGroups.
EnsureInstanceGroupsAndPorts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, done.

@@ -63,8 +63,7 @@ func (i *Instances) Init(zl zoneLister) {
// all of which have the exact same named port.
func (i *Instances) AddInstanceGroup(name string, port int64) ([]*compute.InstanceGroup, *compute.NamedPort, error) {
igs := []*compute.InstanceGroup{}
// TODO: move port naming to namer
namedPort := &compute.NamedPort{Name: fmt.Sprintf("port%v", port), Port: port}
namedPort := utils.GetNamedPort(port)
Copy link
Contributor

Choose a reason for hiding this comment

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

In the next PR when passing all ports, can you please rename AddInstanceGroup to something like SyncInstanceGroups, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. How about EnsureInstanceGroupsAndPorts? Same as the corresponding cluster_manager method name.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good!

@nikhiljindal
Copy link
Contributor Author

Thanks for the detailed review @nicksardo!
Have pushed updated code as per comments. PTAL

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 44.21% when pulling e91ce1511d61cf9d381701220ba734a60c22d30e on nikhiljindal:reportIGName into 587a344 on kubernetes:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 44.234% when pulling 937cde6 on nikhiljindal:reportIGName into 981967b on kubernetes:master.

@nicksardo
Copy link
Contributor

Awesome, everything looks good. Would you mind doing a round of testing for MC but also basic ingress in different states (multiple nodes, multiple ingresses, with default backend, no default backend, etc...)?

@nicksardo nicksardo self-assigned this Sep 13, 2017
@nikhiljindal
Copy link
Contributor Author

Brought up glbc with this code and verified that a single cluster ingress continues to work as expected.
Also verified that it works well with a multi cluster ingress (they reuse instance group).

@nicksardo
Copy link
Contributor

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Sep 14, 2017
@nicksardo nicksardo merged commit b2ad9e7 into kubernetes:master Sep 14, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants