-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
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
Support multi-port service routing for containers running on Marathon #1742
Conversation
2eb5931
to
bc13b74
Compare
Rebased against master, should be good to go. Any other corrections that are required? |
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.
Sorry for the long delay -- real life has been super busy.
I'm half-way through the PR, so I'm sending my first batch of comments out.
I also want to take the opportunity and discuss a design question. What I dislike about the PR is that it adds a whole bunch of new methods and complicates the template significantly. Do you think it might be possible to model everything in terms of services and assign a default
service when no explicit services exist? My thinking is that this would allow us to have a single implementation of the various template functions. (We might need to extend/break the signature for each and every, but personally I'm okay with that as I don't think we consider the template to be part of a stable API.)
Would also be curious to hear @emilevauge's thoughts on the overall design.
provider/marathon/marathon.go
Outdated
@@ -279,6 +298,14 @@ func (p *Provider) applicationFilter(app marathon.Application, filteredTasks []m | |||
}, filteredTasks) | |||
} | |||
|
|||
func (p *Provider) hasApplication(task marathon.Task, apps []marathon.Application) bool { | |||
_, err := getApplication(task, apps) | |||
if 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.
Simplify to return err == nil
.
provider/marathon/marathon.go
Outdated
@@ -420,6 +447,187 @@ func (p *Provider) hasCircuitBreakerLabels(application marathon.Application) boo | |||
return ok | |||
} | |||
|
|||
// Map of services properties | |||
// we can get it with label[serviceName][propertyName] and we got the propertyValue | |||
type labelServiceProperties map[string]map[string]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.
I suggest we add another type for the inner map just for readability reasons.
provider/marathon/marathon.go
Outdated
log.Errorf("Unable to get marathon application from task %s", task.AppID) | ||
return false | ||
} | ||
return len(extractServicesLabels(application.Labels)) > 0 |
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.
You could also just do return hasServices(application)
.
provider/marathon/marathon.go
Outdated
return len(extractServicesLabels(application.Labels)) > 0 | ||
} | ||
|
||
// Extract the service labels from application labels of dockerData struct |
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.
dockerData
does not seem to exist?
(if it's supposed to exist: please choose a name that doesn't contain the word data
.)
provider/marathon/marathon.go
Outdated
|
||
for index, serviceProperty := range *labels { | ||
matches := servicesPropertiesRegexp.FindStringSubmatch(index) | ||
if matches != 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.
Let's check for == nil
and continue
the loop in that case to remove one level of indentation.
provider/marathon/marathon.go
Outdated
} | ||
} | ||
|
||
return v |
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.
Proposal: Instead of the regex-based approach, we could write a little parser. That is, have a piece of code that strips off the components making up our valid syntax and bail out as soon as something doesn't fit.
IMHO, that could make the implementation easier to understand and not require us to maintain a non-trivial regex at all. WDYT?
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 parser route is definitely possible, though I personally prefer the regex as it will be more performant and less code to maintain. After the parser logic is all coded up, it will effectively do the same thing as Regex, scan the label text, tokenize it by dot-delimited groups, build the same result
map with the rest of the logic to follow.
The regex logic is actually not very complex (IMHO).
^traefik\.(?P<service_name>.*?)\.(?P<property_name>port|portIndex|weight|protocol|frontend\.(.*))$
breaks down as following:
- The label MUST start with
traefik.
(use of^
indicates start of line) - The next section, defined by
(?P<service_name>.*?)
group is a named & numbered capturing group (submatch) that will basically extract a piece of text that sits in between.
separators aftertraefik.
segment. - The next section, defined by
(?P<property_name>port|portIndex|weight|protocol|frontend\.(.*))
group is another named & numbered submatch, but conditional onproperty_name
text segment be only one of the listed options:port
,portIndex
,weight
,protocol
orfrontend.*
- The rest of the label till end of line/entry, defined by
$
The reason this regex looks complex is due to using named groups and having to use \
in many places to escape the .
:)
The issues with the parser are:
- instead of 3 lines of code used in regex (regex definition,
servicesPropertiesRegexp.FindStringSubmatch(index)
andservicesPropertiesRegexp.SubexpNames()
) we would have much more code for the parser logic - parser logic would be custom-written, needs to be extensively tested and add complexity that has to be maintained long term
- regex performance is already very good (Google guys have done their part. :) ), the parser might need optimizations to tune it to make sure performance is not suffering.
Thoughts?
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 for the explanation. My main concern is about how the regular expression is applied in the code, and how the regexp package API is used to extract the kind of information we are looking for. To be honest, I think the API isn't the most intuitive, which may stem from the fact that regular expressions aren't the most trivial topic.
Let me share my thoughts on your issues with the parser approach:
- code size. Yes, we'd have somewhat more code. IMO though, I think it'd be easier to understand. When I tried to understand your regex, I basically had to jump back and forth between the regex and the algorithm in
extractServicesLabels
to convince myself that the code is really covering all cases. Personally, I tend to prefer a bit less dense code at the benefit of better comprehensibility. - parser logic/testing. For sure we'd need a custom parser. I disagree on the testing argument though: The regex and the parser approaches should have roughly the same test cases. In fact, I think the more explicit nature of the parser approach make it much clearer to see which cases need to be tested. On the contrary, it's harder to figure out the circumstances under which a regular expression matches or not (which is what took me so long to understand).
- performance. Regexes require full-blown state machines whereas parsing is mostly a matter of simple string comparisons and manipulations. So I have my doubts whether the regex will outperform the parser. More importantly though, I wouldn't worry about this matter at all: It probably takes a lot of regex'ing/parsing for any performance impact to be observable, and even if we think it was, we'd need benchmarks to prove it. For now, let's focus on a solution that's easiest to read, comprehend, and test.
I'll add another point in favor of parsers: they are often simpler to extend. When the structure of what your regular expression has to match changes, chances are you'll have to rebuild the expression or start introducing hard-to-understand features like look-ahead/look-behind. That's usually where things get ugly.
FWIW, the stdlib is full of parsers, so at least the Go team thinks it's a good and fast-enough thing. :)
One implementation speaks louder than 100 discussions, so I went ahead and implemented the parser for comparison reasons. I admit it's a bit larger (especially because your implementation can be further simplified now that we say the service name is mandatory -- I'll address that in a separate comment). OTOH, it's super clear to see what to expect from the label pattern when being parsed, which makes writing down all test cases a rather mechanical matter.
Overall, I'm leaning towards the parser for clarity reasons, but I can see the advantage of a shorter implementation when using a regular expression. I guess I can convince myself that staying with what we have right now in the PR is acceptable, though I wouldn't want to leave this comment thread without a rather complete discussion. :-)
I'll also try to get some additional feedback from the other maintainers on this point once we've covered the remainders of the PR.
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.
Those are fair points. Do you think we can go ahead with this PR as is, and have a separate PR for creating a "generic" parser to handle the label service tokenization and extraction? I can see how that (Parser) can be a generic/common facility used by pretty much any provider that has a "service" concept. (like various Docker derivatives [docker, kubernetes, mesos, marathon, etc.])
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 idea. Okay with me. 👌
provider/marathon/marathon.go
Outdated
|
||
// Gets the entry for a service label searching in all labels of the given application | ||
func getApplicationServiceLabel(application marathon.Application, serviceName string, entry string) (string, bool) { | ||
value, ok := extractServicesLabels(application.Labels)[serviceName][entry] |
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.
AFAICS, this would panic if serviceName
didn't exist in the map. We should cover that case and log a warning.
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 would not panic (just tested the usecase on play.golang.org) The ok
would equal false
and value
would have default string
value of ""
We do check for ok
in all the places we call getApplicationServiceLabel
, like getServicePriority
for example, so that covers the use-case (IMHO)
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.
👍
provider/marathon/marathon.go
Outdated
application, err := getApplication(task, applications) | ||
if err != nil { | ||
log.Errorf("Unable to get marathon application from task %s", task.AppID) | ||
return make([]string, 0, 0) |
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.
Let's just return []string{}
or []string{""}
.
provider/marathon/marathon.go
Outdated
} | ||
// Returning an empty name, as an indication that there are no sub-services, but only main Application | ||
return []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.
I'd change the implementation like this:
- Start off with a
var names []string
declaration. - Iterate over
labelServiceProperties
(without checking the size), appending each key tonames
as we go. - Return
names
.
Seeding the slice with the known length shouldn't be required performance-wise for the number of services we'll usually we dealing with.
provider/marathon/marathon.go
Outdated
@@ -502,7 +710,7 @@ func processPorts(application marathon.Application, task marathon.Task) (int, er | |||
return 0, errors.New("no port found") | |||
} | |||
|
|||
portIndex := 0 | |||
//portIndex := 0 |
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.
Remove the line.
Hello @timoreimann, thanks for the detailed feedback. The current implementation was modeled (with some code copying) from the existing Docker service labels support. I was trying to keep the same design, etc. Let me know if you would like to deviate from how it is currently done in the Docker provider, would be happy to do so. (especially if we are not required to keep the template "API" backwards-compatible). |
8c50caf
to
459ec75
Compare
I'm currently checking back with the other maintainers as to what our understanding is towards any API guarantees. My personal opinion is that compatibility should only be maintained on the higher template rendering level (meaning that the same template input parameters should ideally yield the same output configuration across different Traefik versions) but should not apply to the specific template functions. Once we have clarity on this point, we can decide how to proceed. I'd feel much more comfortable if we had the desirable freedom so that we don't have to add too much complexity to the Marathon provider. Will get back to you on this one ASAP! |
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.
Two other comments.
I'll continue with the review as soon as we have clarity on the template API point, as this is crucial in deciding whether we'll stick to the current implementation or have the freedom to refactor more aggressively.
provider/marathon/marathon.go
Outdated
continue | ||
} | ||
|
||
result := make(map[string]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.
Here's the promised comment I announced in another comment:
I don't think we need the result
map. Our regular expression pattern basically guarantees us that we'll have service_name
and property_name
sub-matches. So we can just extract the names/values at the known index positions and use them to populate the labelServiceProperties
map.
provider/marathon/marathon.go
Outdated
} | ||
serviceName := result["service_name"] | ||
if _, ok := v[serviceName]; !ok { | ||
v[serviceName] = make(map[string]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.
make
can now use serviceProperties
.
provider/marathon/marathon.go
Outdated
@@ -420,6 +444,193 @@ func (p *Provider) hasCircuitBreakerLabels(application marathon.Application) boo | |||
return ok | |||
} | |||
|
|||
// Map of services properties | |||
// we can get it with label[serviceName][propertyName] and we got the propertyValue | |||
type serviceProperties map[string]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.
Since this is a map from property (names) to values, how about calling it servicePropertyValues
?
provider/marathon/marathon.go
Outdated
continue | ||
} | ||
|
||
// Accordig to the regex, match index 1 is "service_name" and match index 2 is the "property_name" |
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.
Typo: Accordig -> According
provider/marathon/marathon.go
Outdated
// Accordig to the regex, match index 1 is "service_name" and match index 2 is the "property_name" | ||
serviceName := matches[1] | ||
propertyName := matches[2] | ||
v[serviceName] = make(servicePropertyValues) |
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.
You dropped the check if the servicePropertyValues
map exists. Aren't you now overwriting any pre-existing value with a brand new map on every loop iteration?
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 catch!
@aantono could you please only push commits (as opposed to rebasing) during the review? Otherwise, it's very hard to see which changes you added. Thanks! |
Tests are failing. :-) |
@aantono looks like we're okay to consider covering the extended service-based functionality by the existing template functions, even if it means adjusting the signatures. I haven't deeply explored the idea but think it should be feasible. Would you mind taking a stab and see how far we can get with this? Thanks! |
@timoreimann, sounds good. I will try to model everything in terms of services and assign a |
Yes, I think we're free to go wild. :-) |
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.
Reflecting a bit, I believe we're finally good on this PR. A quick test with our Marathon cluster also showed no problems.
LGTM -- thanks a lot @aantono, excellent work. 👏
@containous/docker could someone please review the tiny improvement to the Docker regex? |
@aantono can I leave the resolution of the conflicts to you? I probably won't have too much time over the next couple of days. |
Sounds good. I'll rebase as soon as I get home. |
…ional parameter to the template methods.
…y required. Reverted accidental Metrics PR changes and general code cleanup and docs.
… methods to make them more separate.
Additional changes: - Filter tasks with multiple services if at least one service is missing its port. - Extend tests for previous case. - Use %q for logging.
… being invalid value, service does not get filtered
We cannot filter services while the server rendering will eventually drop broken services anyway.
Rebased, should be good to merge now... |
@aantono the build is red now. Something must have gone wrong during your rebase. Could you check please? |
…ices that have some invalid ports.
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.
LGTM 🐸
I'm going to have a final look at the diff. Please don't merge yet. |
@timoreimann Sorry I did not see your comment 😞 |
@ldez no worries, will do next time. Everything looks fine anyway, so no harm done. :) |
Implementation of #1621